diff --git a/.github/workflows/review-submission.yml b/.github/workflows/review-submission.yml new file mode 100644 index 0000000..93f34d3 --- /dev/null +++ b/.github/workflows/review-submission.yml @@ -0,0 +1,52 @@ +name: Review Subdomain Submission + +on: + pull_request_target: + types: + [ + opened, + synchronize, + reopened, + edited, + ready_for_review, + converted_to_draft, + ] + branches: [main] + workflow_dispatch: + inputs: + pr_number: + description: PR number to check and send an email for + required: true + type: string + +permissions: {} + +concurrency: + group: submission-review-${{ github.event.pull_request.number || inputs.pr_number }} + cancel-in-progress: false + +jobs: + review: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pull-requests: write + statuses: write + steps: + # SECURITY: run trusted base code only. Never check out or execute PR code here. + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + ref: ${{ github.event.pull_request.base.sha || github.sha }} + persist-credentials: false + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: "22" + - name: Validate, comment, and notify + run: node scripts/review-pr.js + env: + GITHUB_TOKEN: ${{ github.token }} + REVIEWER_LOGIN: maoawa + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + REVIEW_SERVICE_URL: ${{ vars.REVIEW_SERVICE_URL }} + REVIEW_WEBHOOK_SECRET: ${{ secrets.REVIEW_WEBHOOK_SECRET }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c30ae69 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test Review Automation +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: "22" + - run: npm test + - run: npm ci + working-directory: review-service + - run: npm test + working-directory: review-service + - run: npm run check + working-directory: review-service diff --git a/.gitignore b/.gitignore index 51e7ab2..2d91d44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ /node_modules -package-lock.json \ No newline at end of file +package-lock.json +# Local service state and credentials +**/node_modules/ +**/.wrangler/ +**/.dev.vars* +**/.env* +!**/.dev.vars.example +!review-service/package-lock.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e0e10ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Submission rules / 提交规范 + +For a normal subdomain request, submit a PR to `main` with a short description of your website. The automated reviewer comments on every submission and reports the `submission-format` status. Passing format checks does not approve the website's content. + +普通域名申请请向 `main` 提交 PR,并简要说明网站用途。机器人会检查格式并留言;检查通过不代表内容审核通过。 + +## Quick start + +Copy the README's JSON template, replace the username/domain/DNS target, save it as `records/.json`, and open a PR describing your website in one sentence. The contact email is optional. Keep `proxied` as `false` if you are unsure. The bot will explain any problem and you can edit the same PR to fix it. + +复制 README 模板,填写用户名、域名和 DNS 目标,保存为 `records/<域名>.json`,再提交 PR 并用一句话介绍网站即可。邮箱可不填;不确定代理设置时保留 `false`。机器人会提示问题,修改同一个 PR 即可。 + +## Detailed checks (reference) + +- Change 1–10 files, only directly under `records/`, named `.json`. Automated approval accepts additions and modifications; deletions, renames, and maintenance changes require manual review. +- Each file must be a regular, non-executable UTF-8 JSON object, at most 32 KiB. +- Required fields: `owner`, `domain`, `records`, `proxied`. Optional field: `ttl`. Other fields are rejected to catch typos. +- `owner` contains a valid `username` and optionally `email`. The username must match the PR author, case-insensitively. Email may be omitted or empty; supplied addresses are public. +- `domain` must equal the filename without `.json`. Use lowercase DNS labels separated by dots, each no longer than 63 characters. A leading underscore supports verification labels. No wildcards, slashes, URLs, or trailing dot. +- `records` supports `A`, `AAAA`, `CNAME`, and `TXT`. Each value is a string or an array of 1–20 unique, non-empty strings. A/AAAA must be valid IP addresses. TXT values are at most 2,048 UTF-8 bytes each. +- `CNAME` takes exactly one hostname, without a URL, IP address, or self-reference. It cannot coexist with any other record type. +- `proxied` must be `true` or `false`. Set it to `false` for TXT-only configurations. +- Optional `ttl`: `1` (automatic) or an integer from `60` to `86400` seconds. +- Existing files, parent namespaces, and child namespaces remain reserved for their recorded owners. Delegated requests and ownership transfers need manual review. +- The current deployment script does not remove old record types or non-CNAME values. Such changes must be handled manually, including the DNS cleanup. + +完整示例见 [中文说明](README.md) 或 [English README](README.en.md)。机器人只检查 PR 中变更的配置,不要求一次修复所有旧文件。 + +## Commit messages + +Use a short, readable commit message. **GitHub's default message is fine; no special syntax is required.** For example: + +```text +Create luna.json +Update luna.json +Add my personal blog +修正博客的 DNS 记录 +``` + +The automated check only requires a non-empty first line of at most 200 characters, without control characters. Merge/sync commits are exempt. PRs with more than 100 commits need manual review. + +**不需要学习特殊的提交格式,直接保留 GitHub 默认的提交说明即可。** 也可以用中文或其他语言简要说明变更。首行非空、不超过 200 字符,且不含控制字符即可。 + +## Review results + +Fix the errors in the bot's comment and push again. Draft PRs are not emailed. A passing submission gets a maintainer review; it is never automatically merged just because the check passes. + +For maintenance PRs, the format check deliberately fails with an explanation. A maintainer can review and merge them manually. **Do not make `submission-format` universally required** unless your branch rules provide a deliberate maintenance path. diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..fdb060e --- /dev/null +++ b/README.en.md @@ -0,0 +1,78 @@ +# ฅ●ω●ฅ moe.page — Free, cute subdomains + +[简体中文](README.md) | **English** + +[![GitHub Actions Status](https://github.com/moepage/subdomain/actions/workflows/deploy.yml/badge.svg)](https://github.com/moepage/subdomain/actions) + +> Get your own `.moe.page` subdomain with a pull request! + +## ✨ What is this? + +`moe.page` is a free, open, automated subdomain registration project. It gives your personal projects, blogs, homepages, and creative ideas a cute, memorable address. + +## 🚀 How to apply + +### Step 1: Fork this repository + +Click **Fork** at the top right to copy this project to your GitHub account. + +### Step 2: Add your domain file + +1. Open the `records/` directory in your fork. +2. Select **Add file → Create new file**. +3. Name the file after the subdomain you want, with a `.json` extension. For `luna.moe.page`, use `luna.json`. + +### Step 3: Fill in your configuration + +Copy this template into the new file and replace the example values: + +```json +{ + "owner": { + "username": "your-github-username", + "email": "you@example.com" + }, + "domain": "luna", + "records": { + "A": ["1.2.3.4"] + }, + "proxied": false +} +``` + +- `owner`: Your GitHub username and optional contact email. Your username must match the PR author for automated review. Contact emails committed here are public; omit the field or leave it empty if you prefer. +- `domain`: Your requested subdomain. It must match the filename exactly, in lowercase. +- `records`: Your DNS records. `A` points to an IPv4 address, `AAAA` to an IPv6 address, `CNAME` to another hostname, and `TXT` contains text, often used for verification. Values may be a string or an array of strings. A CNAME must have exactly one target and cannot coexist with any other record type. +- `proxied`: Whether to enable the Cloudflare proxy (orange cloud). This must be a Boolean and applies only to A, AAAA, and CNAME records. +- `ttl` (optional): `1` for automatic TTL, or a number of seconds from `60` to `86400`. The deployment defaults to `120` and uses automatic TTL for proxied records. + +Nested names such as `wiki.luna` and verification labels such as `_atproto.luna` are supported. Existing namespaces cannot be claimed by another owner through automated approval. + +### Step 4: Submit a pull request + +1. Keep GitHub’s default commit message (such as `Create luna.json`) or write a short description. No special commit format is required. +2. Return to your fork's homepage and select **Contribute → Open pull request**. +3. Target `main` and describe what your website does in the PR description. +4. The bot checks commit messages, JSON and DNS formats, and recorded ownership, then leaves or updates a comment explaining the result. +5. After the checks pass, a maintainer reviews the submission. When email review is configured, they receive the submission details and private links to approve and merge, or decline with a message. +6. Once merged, GitHub Actions deploys the DNS records. Check the deployment result; DNS changes can take time to become visible. + +Automated checks validate format, not website safety. Maintenance changes, ownership transfers, record deletion, and changes requiring removal of old DNS values need manual review. + +## 📜 Rules and limitations + +1. Do not use these domains for illegal or inappropriate content, including phishing, spam, or adult content. +2. A CNAME cannot coexist with any other DNS record type. +3. We reserve the right to remove abused domains at any time. + +## Maintainer setup + +See [review automation setup](docs/review-automation.md) for the GitHub workflow, email delivery, and the small Cloudflare review service. The email links show details and require a deliberate button press before changing a PR. + +## Acknowledgments + +Thanks to [Cloudflare](https://www.cloudflare.com/) for its infrastructure and support. + +--- + +Get started with your own `.moe.page` subdomain! diff --git a/README.md b/README.md index 0b7c770..7e9bd70 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# ฅ●ω●ฅ moe.page - 免费的萌系二级域名 +# ฅ●ω●ฅ moe.page - 免费的萌系二级域名 -[![GitHub Actions Status](https://github.com/zknmoe/moe.page-subdomains/actions/workflows/deploy.yml/badge.svg)](https://github.com/zknmoe/moe.page-subdomains/actions) +**简体中文** | [English](README.en.md) + +[![GitHub Actions Status](https://github.com/moepage/subdomain/actions/workflows/deploy.yml/badge.svg)](https://github.com/moepage/subdomain/actions) > 只需一个 Pull Request,即可拥有你的专属 `.moe.page` 域名! @@ -32,29 +34,30 @@ }, "domain": "luna", "records": { - "A": [ - "1.2.3.4" - ] + "A": ["1.2.3.4"] }, "proxied": false } ``` **字段说明:** -* `owner`: 你的个人信息。 -* `domain`: 你想要的子域名(**必须**和文件名一致)。 -* `records`: 你想设置的 DNS 记录。 - * `A`: 指向一个 IPv4 地址。 - * `CNAME`: 指向另一个域名。(**注意:CNAME 很霸道,如果用了它,就不能有其他任何记录!**) - * `TXT`: 可以是任何文本,常用于验证。 -* `proxied`: 是否开启 Cloudflare 代理(橙色云朵),只对 A, AAAA, CNAME 记录有效。 + +- `owner`: 你的个人信息。 +- `domain`: 你想要的子域名(**必须**和文件名一致)。 +- `records`: 你想设置的 DNS 记录。 + - `A`: 指向一个 IPv4 地址。 + - `CNAME`: 指向另一个域名。(**注意:CNAME 很霸道,如果用了它,就不能有其他任何记录!**) + - `TXT`: 可以是任何文本,常用于验证。 +- `proxied`: 是否开启 Cloudflare 代理(橙色云朵),只对 A, AAAA, CNAME 记录有效。 ### 第四步:提交 Pull Request 1. 完成以上步骤后,回到你仓库的首页。 2. 你会看到一个提示,点击 `Contribute` -> `Open pull request`。 3. 简单描述一下你的网站是做什么的,然后提交 PR。 -4. 等待机器人检查通过,以及管理员(就是我啦!)合并后,你的域名就生效啦! +4. 直接保留 GitHub 默认的提交说明(例如 `Create luna.json`),或简单描述变更即可,不需要特殊格式。详细检查规则见 [提交规范](CONTRIBUTING.md)。 +5. 机器人会检查提交信息、JSON / DNS 格式与已有记录的归属,并在 PR 中更新检查结果。检查通过后,管理员可通过邮件中的私密审核页面查看详情、批准并合并,或填写原因拒绝。邮件功能需要先完成管理员配置。 +6. 合并后,GitHub Actions 会部署 DNS 记录,请确认部署成功。 ## 📜 规则与限制 @@ -62,9 +65,14 @@ 2. 请遵守 CNAME 的规则:如果你的记录里有 `CNAME`,就不能有其他任何类型的记录。 3. 我们保留随时删除被滥用的域名的权利。 +## 管理员配置 + +请查看 [自动审核与邮件服务配置](docs/review-automation.md)。自动检查仅验证格式,不判断网站内容是否合规。涉及所有权转移、删除记录或清理旧 DNS 值的变更需要人工处理。`owner.email` 是可选字段,提交到公开仓库后所有人均可查看。 + ## 鸣谢 -* 感谢 [Cloudflare](https://www.cloudflare.com/) 提供的强大支持。 +- 感谢 [Cloudflare](https://www.cloudflare.com/) 提供的强大支持。 --- -现在就开始吧! \ No newline at end of file + +现在就开始吧! diff --git a/docs/review-automation.md b/docs/review-automation.md new file mode 100644 index 0000000..c7d2bd7 --- /dev/null +++ b/docs/review-automation.md @@ -0,0 +1,88 @@ +# PR review and email approval setup + +The GitHub workflow validates submissions and updates a bot comment. Passing submissions can be emailed to one configured maintainer with the applicant's username, PR description, recent discussion, domains, contact email, DNS records, proxy setting, and TTL. The email includes a review link and a decline link. Both open the same compact page; decline scrolls to its message form. No action occurs on GET, HEAD, email preview, or link scanning. HTML and plain-text emails are provided. The dark page requires no JavaScript and supports preset reasons as well as custom text for watch use. + +Apple Watch Mail web views vary by watchOS version, mail client, and settings. The layout and form are designed for a small screen, but confirm both actions on your own watch before relying on it. GitHub links remain available as a fallback. + +## Components + +- `.github/workflows/review-submission.yml`: privileged `pull_request_target` workflow; executes only the trusted base revision. Applicant files are retrieved as bounded data through GitHub's API, never executed or installed. No PR checkout, artifact execution, or shell interpolation of applicant content. +- `scripts/validate-submission.js`: shared format and ownership checks, used in GitHub Actions and again by the service before merging. +- `review-service/`: Cloudflare Worker, D1 database, and native Cloudflare email integration. Approval links contain signed, expiring bearer tokens. They grant authority over one PR revision; keep them private. +- `.github/workflows/test.yml`: unprivileged tests for the validator and the Worker. + +The service is intentionally a separate deployment. Merging these files activates comments and checks; email is skipped with a workflow warning until configured. Do not publish your recipient address or credentials in this public repository. + +## Provision the review service + +Use a Cloudflare account you administer, an Email Routing sender domain, and a verified destination address. Sending to verified destinations is supported on the free plan; this service does not need arbitrary-recipient sending or Resend. These are setup choices; this change does not provision a paid plan, add credentials, or deploy a service automatically. + +```sh +cd review-service +npm ci +npx wrangler login +npx wrangler d1 create moe-page-review +``` + +The checked-in configuration identifies the production account and D1 database. For a different deployment, replace `account_id` and `database_id` with your own identifiers. Keep `database_name`, the Worker name, and `GITHUB_REPOSITORY` aligned with your intended account and repo. Use local or staging resources first. + +```sh +npx wrangler d1 migrations apply moe-page-review --remote +``` + +Set the following Worker secrets using `npx wrangler secret put NAME`. Do not put values in shell command arguments or in the repository. + +| Secret | Value | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `GITHUB_APP_ID` | Numeric ID of the organization-owned GitHub App. | +| `GITHUB_APP_INSTALLATION_ID` | Numeric installation ID for this repository. | +| `GITHUB_APP_PRIVATE_KEY` | The App’s PEM private key. Upload from a protected local file/stdin; both GitHub PKCS#1 and PKCS#8 are supported. | +| `REVIEW_WEBHOOK_SECRET` | A cryptographically random secret of at least 32 characters, shared only with GitHub Actions. | +| `REVIEW_LINK_SECRET` | A separate random secret of at least 32 characters; only the Worker needs this. Rotating it invalidates all existing links. | +| `REVIEW_EMAIL` | The maintainer's destination email address. | +| `EMAIL_FROM` | Plain sender address on the configured domain, e.g. `reviews@your-domain.example`. | +| `PUBLIC_URL` | Canonical HTTPS origin of this Worker, without a path. | + +Create a private GitHub App owned by the repository's organization. Disable webhooks and user OAuth authorization. Grant repository **Contents: read/write**, **Pull requests: read/write**, **Commit statuses: read**, and **Checks: read**; Metadata read access is implicit. Leave other permissions unset. Install it on **only this repository**, with no administrator/ruleset bypass privileges. Contents write is required by GitHub's merge API and also technically allows other content writes; GitHub has no merge-only permission. + +The Worker signs a short-lived App JWT and requests a one-hour installation token restricted to the configured repository and permissions for each request. App reviews and merges are attributed to its bot (`moepage-review[bot]` for this deployment). `REVIEWER_LOGIN` credits the human reviewer with a mention in approval/decline messages; the automated format comment identifies that account as the human-review contact without claiming approval. For squash merges, optional `REVIEWER_COAUTHOR` adds the configured Git identity as a `Co-authored-by` trailer, preserving commit authors and existing co-author trailers. This does not make the human account the GitHub API actor. Unlike the Actions workflow's built-in `GITHUB_TOKEN`, App installation-token merges can trigger the existing push-based DNS deployment. No personal access token or user OAuth client secret is required. + +Configure `send_email` with a binding named `EMAIL` and a sender allowlist matching `EMAIL_FROM`. Without a destination restriction, Cloudflare limits the binding to verified account destinations; the application sends only to the private `REVIEW_EMAIL` setting, never an applicant address. For tighter binding-level controls, use a private deployment config with `destination_address` set to the maintainer. Enable Email Routing for the sender's subdomain and verify the destination first. See [Cloudflare sending bindings](https://developers.cloudflare.com/email-service/configuration/send-bindings/) and [pricing](https://developers.cloudflare.com/email-service/platform/pricing/). Do not upgrade plans for this setup without approval. + +The service calls GitHub's normal merge API with the exact checked head SHA. It does not bypass protection rules, required reviews, or merge queues. All visible commit statuses must be successful and check runs must be complete and successful/neutral/skipped. If GitHub still requires additional reviews, the merge will fail and need completion on GitHub. Use GitHub directly for merge-queue-only repositories. `MERGE_METHOD` defaults to `squash` and can be changed to another method enabled in the repository. Co-author credit is only added for squash merges; rebase merges preserve their original commits. The configured reviewer uses their GitHub no-reply address, not a personal mailbox. + +```sh +npm test +npm run check +npm run deploy +``` + +D1, the App key, and installation tokens remain server-side. The database stores submission snapshots, token identifiers, expiry, and decision states, not raw link signatures. No applicant email is used as the notification destination. Invocation logging is disabled and trace sampling is zero because review URLs contain credentials; structured application logs include only event names and PR numbers. Do not enable raw URL logging or attach third-party analytics to the review pages. + +## Connect GitHub Actions + +In **Repository Settings → Secrets and variables → Actions**: + +1. Add repository variable `REVIEW_SERVICE_URL` with the deployed HTTPS origin. +2. Add repository secret `REVIEW_WEBHOOK_SECRET` with the same value as the Worker's webhook secret. +3. Ensure GitHub Actions is enabled and organization settings allow the workflow's scoped permissions: contents read, pull requests write, and commit statuses write. The workflow does not submit approval reviews itself. +4. Merge the reviewed implementation into `main` to activate the trusted-base workflow. + +New/opened/updated/reopened PRs and PR description/draft-status edits are checked. Existing PRs are not retroactively emailed just by merging the workflow. Run **Review Subdomain Submission → Run workflow**, enter a PR number, or push a new commit to that PR. Workflow dispatch should run from `main`. + +A PR's exact head and base commits are fixed in each link. Changes to the PR or `main` invalidate old links. When `main` advances, manually rerun the review workflow for pending submissions to get fresh emails. Duplicate workflow runs reuse the same email and links for 24 hours; rerunning after expiry issues a new link. Discussion and description in the email/page are the snapshot at the time of the first notification; open GitHub for newer conversation. Descriptions are capped at 8,000 characters and the latest 10 discussion comments at 2,000 characters each. Code-review inline comments are available through the GitHub link. + +## Decisions and recovery + +- **Approve & merge** rechecks the submission, open/draft state, head/base commits, mergeability, commit statuses, and check runs. It acquires a single-use D1 lock, submits an approval review pinned to the head commit, rechecks the PR, then merges that SHA. DNS deployment runs separately afterward. +- **Decline & close PR** requires a preset or custom message. It locks the link, posts a polite request-changes review pinned to the checked commit, checks the revision again, and closes the PR. The review pairs English and Chinese lines for the greeting, preset reason, and closing, includes any additional note exactly as written, and invites a new PR after addressing the feedback. Presets include missing website details, domain/DNS issues, project-rule issues, and inappropriate content. The message is visible in the PR conversation. +- Changes during validation abort the operation. Concurrent or repeated taps cannot acquire the same D1 lock twice. The merge API also atomically rejects a different head SHA. GitHub's close API has no conditional head parameter: there is a small unavoidable race after the final check when closing, so reopen on GitHub if someone pushed at that exact moment. Likewise, the base branch can move between the final read and merge; GitHub's protections are the final gate. +- If a GitHub write partly succeeds, or a request times out after locking, the link remains `error` or `processing`. It is not automatically retried. Inspect the PR on GitHub and complete the action there. Do not reset a lock until you have established what happened. +- Email sends acquire a separate D1 lock. `email_sent` is `0` (not attempted), `2` (sending), `1` (accepted), or `-1` (uncertain failure). The Cloudflare binding provides no idempotency key, so an uncertain send is not automatically retried. Inspect Cloudflare delivery logs; only after establishing non-delivery should an operator reset that row to `0` and rerun the workflow. Successful API acceptance does not guarantee inbox delivery. An explicit rerun after the 24-hour link expiry issues a new review and email. +- The database has no automated purge. Periodically remove expired completed records according to your retention needs; keep uncertain `processing`/`error` records until resolved. + +## Verification before enabling live decisions + +`npm test` at the repository root covers format, DNS, ownership, scope, and unsafe git object handling. `cd review-service && npm test` runs integration tests in Cloudflare's Workers runtime with a real local D1 binding and mocked GitHub/email APIs. `npm run check` bundles the Worker without deployment. + +For a live staging check, use a test repository and a service credential limited to it. Confirm email delivery and both watch forms there, including a changed-commit rejection and a second tap. Do not test merging or declining on an applicant's real PR. Production mail delivery, credentials, branch protection, DNS deployment, and Apple Watch interaction cannot be verified by the mocked tests. diff --git a/package.json b/package.json index 1437afa..b94392b 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test test/*.test.js" }, "repository": { "type": "git", - "url": "git+https://github.com/zknmoe/moe.page-subdomains.git" + "url": "git+https://github.com/moepage/subdomain.git" }, "keywords": [], "author": "", @@ -18,7 +18,7 @@ "axios": "^1.11.0" }, "bugs": { - "url": "https://github.com/zknmoe/moe.page-subdomains/issues" + "url": "https://github.com/moepage/subdomain/issues" }, - "homepage": "https://github.com/zknmoe/moe.page-subdomains#readme" + "homepage": "https://github.com/moepage/subdomain#readme" } diff --git a/review-service/.dev.vars.example b/review-service/.dev.vars.example new file mode 100644 index 0000000..ee123f9 --- /dev/null +++ b/review-service/.dev.vars.example @@ -0,0 +1,9 @@ +# Local development only. Copy to .dev.vars (gitignored); never commit real values. +GITHUB_APP_ID=replace-with-app-id +GITHUB_APP_INSTALLATION_ID=replace-with-installation-id +GITHUB_APP_PRIVATE_KEY="replace-with-PEM-key" +REVIEW_WEBHOOK_SECRET=replace-with-at-least-32-random-characters +REVIEW_LINK_SECRET=replace-with-a-different-random-secret-at-least-32-characters +REVIEW_EMAIL=maintainer@example.com +EMAIL_FROM=reviews@your-verified-domain.example +PUBLIC_URL=https://your-worker.workers.dev diff --git a/review-service/migrations/0001_reviews.sql b/review-service/migrations/0001_reviews.sql new file mode 100644 index 0000000..4394046 --- /dev/null +++ b/review-service/migrations/0001_reviews.sql @@ -0,0 +1,12 @@ +CREATE TABLE reviews ( + id TEXT PRIMARY KEY, + repository TEXT NOT NULL, + pr_number INTEGER NOT NULL, + head TEXT NOT NULL, + base TEXT NOT NULL, + expires INTEGER NOT NULL, + snapshot TEXT NOT NULL, + state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'processing', 'merged', 'declined', 'error')), + email_sent INTEGER NOT NULL DEFAULT 0, + UNIQUE(repository, pr_number, head, base) +); diff --git a/review-service/package-lock.json b/review-service/package-lock.json new file mode 100644 index 0000000..e415421 --- /dev/null +++ b/review-service/package-lock.json @@ -0,0 +1,3533 @@ +{ + "name": "moe-page-review-service", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "moe-page-review-service", + "devDependencies": { + "@cloudflare/vitest-plugin": "1.2.4", + "vitest": "4.1.11", + "wrangler": "4.137.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.2.tgz", + "integrity": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-plugin": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-plugin/-/vitest-plugin-1.2.4.tgz", + "integrity": "sha512-Fyxu+LpDXUFvqTlSReg2qtxM4zhT3sqtUaj+jTwEa4WZtN2NohUsg+zXp6w+aJnP1+FJvdxTYHSRQ31AT0fcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cjs-module-lexer": "1.2.3", + "esbuild": "0.28.1", + "miniflare": "5.20260921.0-alpha", + "wrangler": "4.137.0", + "zod": "4.4.3" + }, + "peerDependencies": { + "@vitest/runner": "^4.1.0", + "@vitest/snapshot": "^4.1.0", + "vitest": "^4.1.0" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260921.1.tgz", + "integrity": "sha512-3iB2WnYOlZ29T+1zhCwbHFExCBp6E9bgmDUMryATYwrIGEQ1YbvR78m4ydm56XKN/d/yF3803ivMGfZMYDtiMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260921.1.tgz", + "integrity": "sha512-FpqVR7IQXVBmGtajyonEmhmb5UAsmV7dTaIkpemmHZXHEw7uYpkhkzKPjc4BOPhNQy8iwt2p+RZBPMY3Y7/bvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260921.1.tgz", + "integrity": "sha512-riAJIohaVp5A8Sqy4yKlzHOaLPOICMf5oey+jC2rm45RVT+wK8+7UU0d31Dy/02Nc8YUkobAFwNVjX06P8WQ5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260921.1.tgz", + "integrity": "sha512-tnJu08tT7s0XWDqp3O0H/vCp0voy9OqVAzspb89biMo1dh8IiEpnyXnoPmdJ7H4qBnXCmXgy0kuEphuvpDPj9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260921.1.tgz", + "integrity": "sha512-VgNcRPstoZMb1G94JTrx+jU24GtkkazNfox0gnF/2fkuXpcfW/M0e0xvdMovYfwt8ZxG5AB2ZNvanD6ufBwiuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/miniflare": { + "version": "5.20260921.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260921.0-alpha.tgz", + "integrity": "sha512-vHH/unOYvV2jA1Q9SdkmzrQhhMoksdwg5jegu6ZeKaaRzgxZhVbt1NdTpQjHF2VTgiBjgP8SiUlUMfruB3N3SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.4", + "undici": "7.29.0", + "workerd": "1.20260921.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/workerd": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260921.1.tgz", + "integrity": "sha512-4HyG7G1W4ksa6tUZ8bV2jxDRWuL5PXnHm9+Z1sjFPb9OZNoYtXz4y7QQRh4ibi0BF/lOmlAVjbhkUqsAVZuUKA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260921.1", + "@cloudflare/workerd-darwin-arm64": "1.20260921.1", + "@cloudflare/workerd-linux-64": "1.20260921.1", + "@cloudflare/workerd-linux-arm64": "1.20260921.1", + "@cloudflare/workerd-windows-64": "1.20260921.1" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.10.tgz", + "integrity": "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.10.tgz", + "integrity": "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.10.tgz", + "integrity": "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.10.tgz", + "integrity": "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.10.tgz", + "integrity": "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.10.tgz", + "integrity": "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.10.tgz", + "integrity": "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.10.tgz", + "integrity": "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.10.tgz", + "integrity": "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.10.tgz", + "integrity": "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.10.tgz", + "integrity": "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.10.tgz", + "integrity": "sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.10.tgz", + "integrity": "sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.10.tgz", + "integrity": "sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.10.tgz", + "integrity": "sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.10.tgz", + "integrity": "sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.10", + "@rolldown/binding-android-arm64": "1.2.10", + "@rolldown/binding-darwin-arm64": "1.2.10", + "@rolldown/binding-darwin-x64": "1.2.10", + "@rolldown/binding-freebsd-x64": "1.2.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.10", + "@rolldown/binding-linux-arm64-gnu": "1.2.10", + "@rolldown/binding-linux-arm64-musl": "1.2.10", + "@rolldown/binding-linux-ppc64-gnu": "1.2.10", + "@rolldown/binding-linux-s390x-gnu": "1.2.10", + "@rolldown/binding-linux-x64-gnu": "1.2.10", + "@rolldown/binding-linux-x64-musl": "1.2.10", + "@rolldown/binding-openharmony-arm64": "1.2.10", + "@rolldown/binding-win32-arm64-msvc": "1.2.10", + "@rolldown/binding-win32-x64-msvc": "1.2.10" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrangler": { + "version": "4.137.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.137.0.tgz", + "integrity": "sha512-vq2JmxkvwOjnsMUejQwd89/EK6u1d20OMlGUVVmb55gVi2zSBKp2rvmxUiPqrSEntK8NwzpTF3DQ/S2I9/TLsg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "5.20260921.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260921.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260921.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260921.1.tgz", + "integrity": "sha512-3iB2WnYOlZ29T+1zhCwbHFExCBp6E9bgmDUMryATYwrIGEQ1YbvR78m4ydm56XKN/d/yF3803ivMGfZMYDtiMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260921.1.tgz", + "integrity": "sha512-FpqVR7IQXVBmGtajyonEmhmb5UAsmV7dTaIkpemmHZXHEw7uYpkhkzKPjc4BOPhNQy8iwt2p+RZBPMY3Y7/bvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260921.1.tgz", + "integrity": "sha512-riAJIohaVp5A8Sqy4yKlzHOaLPOICMf5oey+jC2rm45RVT+wK8+7UU0d31Dy/02Nc8YUkobAFwNVjX06P8WQ5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260921.1.tgz", + "integrity": "sha512-tnJu08tT7s0XWDqp3O0H/vCp0voy9OqVAzspb89biMo1dh8IiEpnyXnoPmdJ7H4qBnXCmXgy0kuEphuvpDPj9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260921.1.tgz", + "integrity": "sha512-VgNcRPstoZMb1G94JTrx+jU24GtkkazNfox0gnF/2fkuXpcfW/M0e0xvdMovYfwt8ZxG5AB2ZNvanD6ufBwiuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "5.20260921.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260921.0-alpha.tgz", + "integrity": "sha512-vHH/unOYvV2jA1Q9SdkmzrQhhMoksdwg5jegu6ZeKaaRzgxZhVbt1NdTpQjHF2VTgiBjgP8SiUlUMfruB3N3SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.4", + "undici": "7.29.0", + "workerd": "1.20260921.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/wrangler/node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20260921.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260921.1.tgz", + "integrity": "sha512-4HyG7G1W4ksa6tUZ8bV2jxDRWuL5PXnHm9+Z1sjFPb9OZNoYtXz4y7QQRh4ibi0BF/lOmlAVjbhkUqsAVZuUKA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260921.1", + "@cloudflare/workerd-darwin-arm64": "1.20260921.1", + "@cloudflare/workerd-linux-64": "1.20260921.1", + "@cloudflare/workerd-linux-arm64": "1.20260921.1", + "@cloudflare/workerd-windows-64": "1.20260921.1" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/review-service/package.json b/review-service/package.json new file mode 100644 index 0000000..a32160e --- /dev/null +++ b/review-service/package.json @@ -0,0 +1,16 @@ +{ + "name": "moe-page-review-service", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "check": "wrangler deploy --dry-run", + "test": "vitest run" + }, + "devDependencies": { + "@cloudflare/vitest-plugin": "1.2.4", + "vitest": "4.1.11", + "wrangler": "4.137.0" + } +} diff --git a/review-service/src/github-app.js b/review-service/src/github-app.js new file mode 100644 index 0000000..53fb315 --- /dev/null +++ b/review-service/src/github-app.js @@ -0,0 +1,64 @@ +import { createPrivateKey } from "node:crypto"; +import { githubClient } from "../../scripts/github.js"; + +const encoded = (value) => Buffer.from(value).toString("base64url"); + +// Mint per request: no cross-request credential or promise state in the isolate. +export async function appClient(env) { + if ( + !/^\d+$/.test(env.GITHUB_APP_ID ?? "") || + !/^\d+$/.test(env.GITHUB_APP_INSTALLATION_ID ?? "") || + !/^[\w.-]+\/[\w.-]+$/.test(env.GITHUB_REPOSITORY ?? "") + ) + throw new Error("GitHub App configuration is missing or invalid."); + const now = Math.floor(Date.now() / 1000); + const payload = `${encoded(JSON.stringify({ alg: "RS256", typ: "JWT" }))}.${encoded(JSON.stringify({ iat: now - 60, exp: now + 540, iss: env.GITHUB_APP_ID }))}`; + // GitHub downloads PKCS#1 PEM; Web Crypto requires PKCS#8. + const pem = createPrivateKey(env.GITHUB_APP_PRIVATE_KEY); + const key = await crypto.subtle.importKey( + "pkcs8", + pem.export({ type: "pkcs8", format: "der" }), + { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + false, + ["sign"], + ); + const signature = await crypto.subtle.sign( + "RSASSA-PKCS1-v1_5", + key, + new TextEncoder().encode(payload), + ); + const response = await fetch( + `https://api.github.com/app/installations/${env.GITHUB_APP_INSTALLATION_ID}/access_tokens`, + { + method: "POST", + headers: { + Authorization: `Bearer ${payload}.${encoded(new Uint8Array(signature))}`, + Accept: "application/vnd.github+json", + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "moe-page-review", + }, + body: JSON.stringify({ + repositories: [env.GITHUB_REPOSITORY.split("/")[1]], + permissions: { + contents: "write", + pull_requests: "write", + checks: "read", + statuses: "read", + }, + }), + signal: AbortSignal.timeout(15000), + }, + ); + if (!response.ok) + throw new Error(`GitHub App authentication failed (${response.status}).`); + const result = await response.json(); + if ( + typeof result.token !== "string" || + !result.token || + Date.parse(result.expires_at) <= Date.now() || + !Number.isFinite(Date.parse(result.expires_at)) + ) + throw new Error("GitHub returned an invalid installation token."); + return githubClient(result.token, env.GITHUB_REPOSITORY); +} diff --git a/review-service/src/messages.js b/review-service/src/messages.js new file mode 100644 index 0000000..3305011 --- /dev/null +++ b/review-service/src/messages.js @@ -0,0 +1,78 @@ +export const declineReasons = [ + { + value: "Please describe the purpose of your website more clearly.", + en: "More website details needed", + zh: "请补充网站信息", + }, + { + value: + "The requested domain or DNS records are not suitable. Please revise your submission.", + en: "Domain or DNS issue", + zh: "域名或 DNS 记录需要调整", + }, + { + value: "This submission does not meet the project's usage rules.", + en: "Does not meet project rules", + zh: "不符合项目使用规则", + }, + { + value: "inappropriate-content", + en: "Inappropriate content", + zh: "网站包含不适宜的内容", + }, +]; + +export function reviewerMention(login) { + return /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i.test(login ?? "") + ? `@${login}` + : "the maintainer"; +} + +export function approvalComment(login) { + const reviewer = reviewerMention(login); + return `Thanks for your submission! It has been reviewed and approved by ${reviewer}. \n感谢您的提交!本申请已由 ${reviewer === "the maintainer" ? "管理员" : reviewer} 审核通过。`; +} + +export function squashCommitMessage(commits, login, coauthor) { + const authors = new Map(); + function add(name, email) { + if ( + typeof name !== "string" || + typeof email !== "string" || + !name.trim() || + /[\r\n<>]/.test(name) || + !/^[^\s<>@]+@[^\s<>@]+$/.test(email) + ) + return; + authors.set(email.toLowerCase(), `${name.trim()} <${email}>`); + } + // Preserve the existing commit authors and co-author trailers when supplying + // our own squash message. These are attribution only, never authorization. + for (const item of commits) { + add(item.commit?.author?.name, item.commit?.author?.email); + for (const match of (item.commit?.message ?? "").matchAll( + /^Co-authored-by:\s*([^<>\r\n]+) <([^<>\r\n]+)>\s*$/gim, + )) + add(match[1], match[2]); + } + const reviewer = /^([^<>\r\n]+) <([^<>\r\n]+)>$/.exec(coauthor ?? ""); + if (!reviewer) throw new Error("Invalid reviewer co-author configuration."); + add(reviewer[1], reviewer[2]); + return `Reviewed and approved by ${reviewerMention(login)}.\n\n${[...authors.values()].map((author) => `Co-authored-by: ${author}`).join("\n")}`; +} + +export function declineComment(selected, custom, login) { + const reason = selected?.trim(); + const note = custom?.trim(); + const preset = declineReasons.find((item) => item.value === reason); + if (reason && reason !== "custom" && !preset) return null; + if ((!preset && !note) || note?.length > 1000) return null; + // Two trailing spaces preserve line-by-line translations in GitHub Markdown. + const explanation = [preset && `${preset.en} \n${preset.zh}`, note] + .filter(Boolean) + .join("\n\n"); + const attribution = login + ? `Your submission has been reviewed by ${reviewerMention(login)}. \n本申请已由 ${reviewerMention(login)} 审核。\n\n` + : ""; + return `Hi! Thanks for your submission. After reviewing it, we're unable to accept it for the following reason: \n您好!感谢您的提交。经审核,我们暂时无法接受此申请,原因如下:\n\n${explanation}\n\n${attribution}You're welcome to open a new pull request once you've addressed this feedback. Thank you for understanding~(∠·ω< )⌒★ \n处理完这些反馈后,欢迎您提交新的 Pull Request。感谢您的理解~(∠·ω< )⌒★`; +} diff --git a/review-service/src/security.js b/review-service/src/security.js new file mode 100644 index 0000000..229ed3d --- /dev/null +++ b/review-service/src/security.js @@ -0,0 +1,69 @@ +const encoder = new TextEncoder(); +async function key(secret) { + if (!secret || secret.length < 32) + throw new Error("Missing or weak signing secret."); + return crypto.subtle.importKey( + "raw", + encoder.encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], + ); +} +const hex = (buffer) => + Array.from(new Uint8Array(buffer), (value) => + value.toString(16).padStart(2, "0"), + ).join(""); +export async function sign(secret, value) { + return hex( + await crypto.subtle.sign("HMAC", await key(secret), encoder.encode(value)), + ); +} +export async function verify(secret, value, signature) { + if (!/^[a-f\d]{64}$/.test(signature ?? "")) return false; + return crypto.subtle.verify( + "HMAC", + await key(secret), + Uint8Array.from(signature.match(/../g), (byte) => parseInt(byte, 16)), + encoder.encode(value), + ); +} +export async function tokenFor(row, secret) { + const value = `${row.id}.${row.expires}`; + return `${value}.${await sign(secret, value)}`; +} +export async function parseToken( + token, + secret, + now = Math.floor(Date.now() / 1000), +) { + if (typeof token !== "string" || token.length > 200) return null; + const [id, expiry, signature, extra] = token.split("."); + if ( + extra !== undefined || + !/^[a-f\d-]{36}$/.test(id ?? "") || + !/^\d{10}$/.test(expiry ?? "") || + Number(expiry) <= now + ) + return null; + if (!(await verify(secret, `${id}.${expiry}`, signature))) return null; + return { id, expires: Number(expiry) }; +} +export async function boundedText(request, maximum) { + const reader = request.body?.getReader(); + if (!reader) return ""; + const decoder = new TextDecoder(); + let bytes = 0; + let output = ""; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + bytes += value.byteLength; + if (bytes > maximum) { + await reader.cancel(); + throw new Error("Request too large."); + } + output += decoder.decode(value, { stream: true }); + } + return output + decoder.decode(); +} diff --git a/review-service/src/views.js b/review-service/src/views.js new file mode 100644 index 0000000..0c6eef8 --- /dev/null +++ b/review-service/src/views.js @@ -0,0 +1,59 @@ +import { declineReasons } from "./messages.js"; + +export const escape = (value) => + String(value ?? "").replace( + /[&<>"']/g, + (char) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ + char + ], + ); +export function details(snapshot) { + const { pr, files, comments = [] } = snapshot; + return `

PR #${pr.number}: ${escape(pr.title)}

GitHub user: ${escape(pr.user.login)}

PR description:

${escape(pr.body || "(No description)")}
${files + .map((file) => { + const record = JSON.parse(file.text); + return `

${escape(record.domain)}.moe.page

Owner: ${escape(record.owner.username)}
Contact email: ${escape(record.owner.email || "(Not provided)")}
Change: ${escape(file.status)}
Cloudflare proxy: ${record.proxied ? "On" : "Off"}
TTL: ${record.proxied ? "Automatic for proxied records; otherwise " : ""}${escape(record.ttl ?? "120 (default)")}

${escape(JSON.stringify(record.records, null, 2))}
${file.previousText === undefined ? "" : `
Previous configuration
${escape(file.previousText)}
`}
`; + }) + .join( + "", + )}

Recent discussion

${comments.length ? comments.map((comment) => `

${escape(comment.user.login)}

${escape(comment.body)}
`).join("") : "

No comments at the time of notification.

"}

Revision ${escape(pr.head.sha.slice(0, 12))}. The excerpt includes the latest 10 discussion comments (up to 2,000 characters each); open GitHub for the full discussion and diff.

`; +} +export function detailsText(snapshot) { + const { pr, files, comments = [] } = snapshot; + return [ + `PR #${pr.number}: ${pr.title}`, + `GitHub user: ${pr.user.login}`, + `PR description:\n${pr.body || "(No description)"}`, + ...files.map((file) => { + const record = JSON.parse(file.text); + return `Domain: ${record.domain}.moe.page\nOwner: ${record.owner.username}\nContact email: ${record.owner.email || "(Not provided)"}\nChange: ${file.status}\nProxy: ${record.proxied}\nTTL: ${record.proxied ? "Automatic for proxied records; otherwise " : ""}${record.ttl ?? 120}\nRecords:\n${JSON.stringify(record.records, null, 2)}${file.previousText === undefined ? "" : `\nPrevious configuration:\n${file.previousText}`}`; + }), + "Recent discussion (latest 10; 2,000 characters each):", + ...comments.map((comment) => `${comment.user.login}: ${comment.body}`), + `Revision: ${pr.head.sha}`, + ].join("\n\n"); +} +export function page(title, content, status = 200) { + return new Response( + `${escape(title)} · moe.page

${escape(title)}

${content}`, + { + status, + headers: { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": + "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", + "X-Frame-Options": "DENY", + }, + }, + ); +} +export function reviewPage(snapshot, token, prUrl) { + return page( + "Review submission", + `

Format checks passed. Review the website and DNS details before deciding. Opening this page does not change the PR.

${details(snapshot)}

Open full PR on GitHub

The selected reason and any additional note will be posted publicly in the PR before it is closed. If you choose a custom reason, enter it above.

The links expire after 24 hours and stop working when the PR revision or main branch changes. Keep this email private: anyone with the link can act on this submission.

`, + ); +} diff --git a/review-service/src/worker.js b/review-service/src/worker.js new file mode 100644 index 0000000..2410629 --- /dev/null +++ b/review-service/src/worker.js @@ -0,0 +1,405 @@ +import { collectReview } from "../../scripts/github.js"; +import { appClient } from "./github-app.js"; +import { + approvalComment, + declineComment, + squashCommitMessage, +} from "./messages.js"; +import { boundedText, parseToken, tokenFor, verify } from "./security.js"; +import { details, detailsText, escape, page, reviewPage } from "./views.js"; + +const now = () => Math.floor(Date.now() / 1000); +const prUrl = (env, number) => + `https://github.com/${env.GITHUB_REPOSITORY}/pull/${number}`; + +const problem = (message, status = 409) => + page("Review unavailable", `

${escape(message)}

`, status); + +async function current(api, row) { + const pr = await api(`/pulls/${row.pr_number}`); + if ( + pr.state !== "open" || + pr.draft || + pr.head.sha !== row.head || + pr.base.sha !== row.base || + pr.base.ref !== "main" + ) + throw new Error( + "The PR is closed, draft, or has changed. Use a fresh review email.", + ); + return pr; +} + +export async function checksPassed(api, head) { + const [status, checks] = await Promise.all([ + api(`/commits/${head}/status?per_page=100`), + api(`/commits/${head}/check-runs?per_page=100&filter=latest`), + ]); + return ( + status.state === "success" && + status.statuses.some( + (item) => + item.context === "submission-format" && item.state === "success", + ) && + checks.total_count <= 100 && + checks.check_runs.every( + (check) => + check.status === "completed" && + ["success", "neutral", "skipped"].includes(check.conclusion), + ) + ); +} + +async function snapshotFor(api, review) { + const { pr, files } = review; + const pageNumber = Math.max(1, Math.ceil(pr.comments / 100)); + let comments = await api( + `/issues/${pr.number}/comments?per_page=100&page=${pageNumber}`, + ); + if (comments.length < 10 && pageNumber > 1) + comments = [ + ...(await api( + `/issues/${pr.number}/comments?per_page=100&page=${pageNumber - 1}`, + )), + ...comments, + ]; + return { + pr: { + number: pr.number, + title: pr.title, + body: (pr.body ?? "").slice(0, 8000), + user: { login: pr.user.login }, + head: { sha: pr.head.sha }, + }, + files: files.map(({ filename, status, text, previousText }) => ({ + filename, + status, + text, + previousText, + })), + comments: comments + .filter((comment) => comment.user.type !== "Bot") + .slice(-10) + .map((comment) => ({ + user: { login: comment.user.login }, + body: comment.body.slice(0, 2000), + })), + }; +} + +async function notify(request, env) { + const payload = await boundedText(request, 4096); + const timestamp = request.headers.get("X-Review-Timestamp"); + if ( + !/^\d{10}$/.test(timestamp ?? "") || + Math.abs(now() - Number(timestamp)) > 300 || + !(await verify( + env.REVIEW_WEBHOOK_SECRET, + `${timestamp}.${payload}`, + request.headers.get("X-Review-Signature"), + )) + ) + return problem("Unauthorized.", 401); + const input = JSON.parse(payload); + if ( + input.repository !== env.GITHUB_REPOSITORY || + !Number.isSafeInteger(input.number) || + input.number < 1 || + !/^[a-f\d]{40}$/.test(input.head ?? "") || + !/^[a-f\d]{40}$/.test(input.base ?? "") + ) + return problem("Invalid notification.", 400); + if ( + !env.REVIEW_EMAIL || + !env.EMAIL_FROM || + !env.EMAIL?.send || + !env.PUBLIC_URL?.startsWith("https://") + ) + return problem("Email delivery is not configured.", 503); + const api = await appClient(env); + const review = await collectReview(api, input.number); + if ( + !review.passed || + review.pr.head.sha !== input.head || + review.pr.base.sha !== input.base + ) + return problem("Submission no longer passes the review."); + // Retried notifications for the same revision reuse the same review record. + await env.DB.prepare( + "INSERT OR IGNORE INTO reviews (id, repository, pr_number, head, base, expires, snapshot) VALUES (?, ?, ?, ?, ?, ?, ?)", + ) + .bind( + crypto.randomUUID(), + env.GITHUB_REPOSITORY, + input.number, + input.head, + input.base, + now() + 86400, + JSON.stringify(await snapshotFor(api, review)), + ) + .run(); + let row = await env.DB.prepare( + "SELECT * FROM reviews WHERE repository = ? AND pr_number = ? AND head = ? AND base = ?", + ) + .bind(env.GITHUB_REPOSITORY, input.number, input.head, input.base) + .first(); + // An explicit workflow rerun after expiry issues a new token and email. + if (row.expires <= now() && row.state === "pending") { + await env.DB.prepare( + "UPDATE reviews SET id = ?, expires = ?, email_sent = 0, snapshot = ? WHERE id = ? AND state = ? AND expires <= ?", + ) + .bind( + crypto.randomUUID(), + now() + 86400, + JSON.stringify(await snapshotFor(api, review)), + row.id, + "pending", + now(), + ) + .run(); + row = await env.DB.prepare( + "SELECT * FROM reviews WHERE repository = ? AND pr_number = ? AND head = ? AND base = ?", + ) + .bind(env.GITHUB_REPOSITORY, input.number, input.head, input.base) + .first(); + } + if (row.email_sent === 1 || row.state !== "pending") + return Response.json({ accepted: true, duplicate: true }); + // Reserve the send before contacting the provider. An uncertain result is + // never retried automatically because the binding has no idempotency key. + const sendLock = await env.DB.prepare( + "UPDATE reviews SET email_sent = 2 WHERE id = ? AND email_sent = 0 AND state = 'pending'", + ) + .bind(row.id) + .run(); + if (sendLock.meta.changes !== 1) + return problem( + "Email sending is in progress or needs manual delivery verification.", + 503, + ); + const snapshot = JSON.parse(row.snapshot); + const token = await tokenFor(row, env.REVIEW_LINK_SECRET); + const link = new URL("/review", env.PUBLIC_URL); + link.searchParams.set("token", token); + const decline = `${link}#decline`; + try { + await env.EMAIL.send({ + from: env.EMAIL_FROM, + to: env.REVIEW_EMAIL, + subject: `moe.page: review PR #${row.pr_number} by ${snapshot.pr.user.login}`, + text: `${detailsText(snapshot)}\n\nReview and approve: ${link}\nDecline with a message: ${decline}\nGitHub: ${prUrl(env, row.pr_number)}\n\nLinks expire in 24 hours. Opening a link does not perform an action. Keep the links private. Website content still needs your review.`, + html: `

Submission ready for your review

Format checks passed. Website content still needs your review.

${details(snapshot)}

Review & approve

Decline with a message

View PR on GitHub

Links expire in 24 hours. Opening a link does not perform an action. Keep the links private.

`, + }); + } catch { + await env.DB.prepare("UPDATE reviews SET email_sent = -1 WHERE id = ?") + .bind(row.id) + .run(); + console.error( + JSON.stringify({ + event: "review_email_needs_recovery", + pr: row.pr_number, + }), + ); + return problem( + "Email delivery could not be confirmed. Check the provider delivery log before manually retrying.", + 502, + ); + } + await env.DB.prepare("UPDATE reviews SET email_sent = 1 WHERE id = ?") + .bind(row.id) + .run(); + console.log( + JSON.stringify({ event: "review_email_sent", pr: row.pr_number }), + ); + return Response.json({ accepted: true }); +} + +async function lookup(env, token) { + const parsed = await parseToken(token, env.REVIEW_LINK_SECRET); + if (!parsed) return null; + const row = await env.DB.prepare("SELECT * FROM reviews WHERE id = ?") + .bind(parsed.id) + .first(); + return row && + row.expires === parsed.expires && + row.repository === env.GITHUB_REPOSITORY + ? row + : null; +} + +async function decide(request, env) { + const origin = request.headers.get("Origin"); + const fetchSite = request.headers.get("Sec-Fetch-Site"); + if ( + (origin && origin !== new URL(env.PUBLIC_URL).origin) || + fetchSite === "cross-site" + ) + return problem("Cross-site requests are not allowed.", 403); + if ( + !request.headers + .get("Content-Type") + ?.startsWith("application/x-www-form-urlencoded") + ) + return problem("Use the review form.", 415); + const form = new URLSearchParams(await boundedText(request, 8192)); + const row = await lookup(env, form.get("token")); + if (!row || row.state !== "pending") + return problem( + "This link is expired, already used, or awaiting manual recovery.", + 410, + ); + const action = form.get("action"); + if (!["approve", "decline"].includes(action)) + return problem("Invalid action.", 400); + const declineBody = declineComment( + form.get("reason"), + form.get("message"), + env.REVIEWER_LOGIN, + ); + if (action === "decline" && !declineBody) + return problem( + "Choose or enter a decline message (1–1000 characters).", + 400, + ); + const api = await appClient(env); + try { + await current(api, row); + } catch (error) { + return problem(error.message); + } + let commitMessage; + if (action === "approve") { + const review = await collectReview(api, row.pr_number); + if ( + !review.passed || + review.pr.head.sha !== row.head || + review.pr.base.sha !== row.base || + review.pr.mergeable !== true || + !(await checksPassed(api, row.head)) + ) + return problem( + "Checks, branch rules, or mergeability are not ready. Retry later or open GitHub.", + ); + if ((env.MERGE_METHOD || "squash") === "squash" && env.REVIEWER_COAUTHOR) + commitMessage = squashCommitMessage( + review.commits, + env.REVIEWER_LOGIN, + env.REVIEWER_COAUTHOR, + ); + } + // A conditional D1 write serializes concurrent approve/decline requests. + const lock = await env.DB.prepare( + "UPDATE reviews SET state = 'processing' WHERE id = ? AND state = 'pending' AND expires > ?", + ) + .bind(row.id, now()) + .run(); + if (lock.meta.changes !== 1) + return problem("Another request already used this link.", 409); + try { + await current(api, row); + if (action === "approve") { + await api(`/pulls/${row.pr_number}/reviews`, { + method: "POST", + body: { + commit_id: row.head, + event: "APPROVE", + body: approvalComment(env.REVIEWER_LOGIN), + }, + }); + await current(api, row); + const merged = await api(`/pulls/${row.pr_number}/merge`, { + method: "PUT", + body: { + sha: row.head, + merge_method: env.MERGE_METHOD || "squash", + ...(commitMessage ? { commit_message: commitMessage } : {}), + }, + }); + if (!merged.merged) throw new Error("GitHub did not merge the PR."); + } else { + // A review pins the decline explanation to the revision that was actually read. + await api(`/pulls/${row.pr_number}/reviews`, { + method: "POST", + body: { + commit_id: row.head, + event: "REQUEST_CHANGES", + body: declineBody, + }, + }); + await current(api, row); + await api(`/pulls/${row.pr_number}`, { + method: "PATCH", + body: { state: "closed" }, + }); + } + await env.DB.prepare("UPDATE reviews SET state = ? WHERE id = ?") + .bind(action === "approve" ? "merged" : "declined", row.id) + .run(); + console.log( + JSON.stringify({ + event: action === "approve" ? "pr_merged" : "pr_declined", + pr: row.pr_number, + }), + ); + return page( + action === "approve" ? "Approved & merged" : "Declined", + `

${action === "approve" ? "GitHub merged this submission. The repository’s DNS deployment will run next; check GitHub for its result." : "Your message was posted as a review and the PR was closed."}

View PR on GitHub

`, + ); + } catch { + // Never automatically retry a partially completed GitHub write. + await env.DB.prepare("UPDATE reviews SET state = 'error' WHERE id = ?") + .bind(row.id) + .run(); + console.error( + JSON.stringify({ + event: "decision_needs_manual_recovery", + pr: row.pr_number, + }), + ); + return page( + "Check GitHub", + `

The operation did not finish cleanly. Some steps may have succeeded. This link is disabled to prevent duplicate actions; finish the review on GitHub.

Open PR

`, + 502, + ); + } +} + +export default { + async fetch(request, env) { + const url = new URL(request.url); + try { + if (request.method === "POST" && url.pathname === "/notify") + return await notify(request, env); + if (request.method === "POST" && url.pathname === "/decision") + return await decide(request, env); + if (request.method === "GET" && url.pathname === "/review") { + const token = url.searchParams.get("token"); + const row = await lookup(env, token); + if (!row || row.state !== "pending") + return problem( + "This link is expired or already used. Request a fresh email or open GitHub.", + 410, + ); + try { + await current(await appClient(env), row); + } catch (error) { + return problem(error.message); + } + return reviewPage( + JSON.parse(row.snapshot), + token, + prUrl(env, row.pr_number), + ); + } + return problem("Not found.", 404); + } catch { + console.error( + JSON.stringify({ event: "review_request_failed", path: url.pathname }), + ); + return problem( + "The review service could not complete the request. Check the service configuration or use GitHub.", + 500, + ); + } + }, +}; diff --git a/review-service/test/github-app.test.js b/review-service/test/github-app.test.js new file mode 100644 index 0000000..8ef1e74 --- /dev/null +++ b/review-service/test/github-app.test.js @@ -0,0 +1,84 @@ +import { generateKeyPairSync, verify } from "node:crypto"; +import { afterEach, expect, test, vi } from "vitest"; +import { appClient } from "../src/github-app.js"; + +const keys = generateKeyPairSync("rsa", { modulusLength: 2048 }); +const config = { + GITHUB_APP_ID: "123", + GITHUB_APP_INSTALLATION_ID: "456", + GITHUB_APP_PRIVATE_KEY: keys.privateKey.export({ + type: "pkcs1", + format: "pem", + }), + GITHUB_REPOSITORY: "moepage/subdomain", +}; +afterEach(() => vi.unstubAllGlobals()); + +test("signs a valid short-lived App JWT and restricts installation tokens", async () => { + const fetcher = vi.fn(async (url, options) => { + if (url.endsWith("/access_tokens")) { + expect(url).toBe( + "https://api.github.com/app/installations/456/access_tokens", + ); + const jwt = options.headers.Authorization.slice(7); + const [header, payload, signature] = jwt.split("."); + expect(JSON.parse(Buffer.from(header, "base64url"))).toEqual({ + alg: "RS256", + typ: "JWT", + }); + expect( + verify( + "RSA-SHA256", + Buffer.from(`${header}.${payload}`), + keys.publicKey, + Buffer.from(signature, "base64url"), + ), + ).toBe(true); + const claims = JSON.parse(Buffer.from(payload, "base64url")); + expect(claims.iss).toBe("123"); + expect(claims.exp - claims.iat).toBe(600); + expect(claims.exp).toBeGreaterThan(Date.now() / 1000); + expect(JSON.parse(options.body)).toEqual({ + repositories: ["subdomain"], + permissions: { + contents: "write", + pull_requests: "write", + checks: "read", + statuses: "read", + }, + }); + return Response.json({ + token: "installation-test-token", + expires_at: new Date(Date.now() + 3600000).toISOString(), + }); + } + expect(options.headers.Authorization).toBe( + "Bearer installation-test-token", + ); + return Response.json({ number: 7 }); + }); + vi.stubGlobal("fetch", fetcher); + const api = await appClient(config); + expect(await api("/pulls/7")).toEqual({ number: 7 }); + expect(fetcher).toHaveBeenCalledTimes(2); +}); + +test("authentication errors and expired tokens fail closed", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response("denied", { status: 403 })), + ); + await expect(appClient(config)).rejects.toThrow( + "authentication failed (403)", + ); + vi.stubGlobal( + "fetch", + vi.fn(async () => + Response.json({ token: "expired", expires_at: "2000-01-01T00:00:00Z" }), + ), + ); + await expect(appClient(config)).rejects.toThrow("invalid installation token"); + await expect( + appClient({ ...config, GITHUB_APP_INSTALLATION_ID: "../invalid" }), + ).rejects.toThrow("configuration"); +}); diff --git a/review-service/test/messages.test.js b/review-service/test/messages.test.js new file mode 100644 index 0000000..b8c8628 --- /dev/null +++ b/review-service/test/messages.test.js @@ -0,0 +1,28 @@ +import { expect, test } from "vitest"; +import { squashCommitMessage } from "../src/messages.js"; + +test("squash attribution retains contributors and adds the reviewer once", () => { + const message = squashCommitMessage( + [ + { + commit: { + author: { name: "Alice", email: "alice@example.com" }, + message: "Add a record\n\nCo-authored-by: Bob ", + }, + }, + { + commit: { + author: { name: "Alice", email: "alice@example.com" }, + message: + "Update\n\nCo-authored-by: maoawa <85821597+maoawa@users.noreply.github.com>", + }, + }, + ], + "maoawa", + "maoawa <85821597+maoawa@users.noreply.github.com>", + ); + expect(message).toContain("Co-authored-by: Alice "); + expect(message).toContain("Co-authored-by: Bob "); + expect(message.match(/Co-authored-by: maoawa/g)).toHaveLength(1); + expect(message).toContain("Reviewed and approved by @maoawa."); +}); diff --git a/review-service/test/worker.test.js b/review-service/test/worker.test.js new file mode 100644 index 0000000..f24e5e1 --- /dev/null +++ b/review-service/test/worker.test.js @@ -0,0 +1,477 @@ +import { env } from "cloudflare:workers"; +import { beforeEach, afterEach, expect, test, vi } from "vitest"; +import worker from "../src/worker.js"; +import { parseToken, sign, tokenFor } from "../src/security.js"; +import migration from "../migrations/0001_reviews.sql?raw"; + +vi.mock("../src/github-app.js", async () => { + const { githubClient } = await import("../../scripts/github.js"); + return { + appClient: async (config) => + githubClient(config.GITHUB_TOKEN, config.GITHUB_REPOSITORY), + }; +}); + +const config = { + GITHUB_REPOSITORY: "moepage/subdomain", + GITHUB_TOKEN: "test-only", + PUBLIC_URL: "https://review.example.com", + REVIEW_LINK_SECRET: "local-link-secret-".repeat(3), + REVIEW_WEBHOOK_SECRET: "local-webhook-secret-".repeat(3), + REVIEW_EMAIL: "maintainer@example.com", + EMAIL_FROM: "review@example.com", + + MERGE_METHOD: "squash", + REVIEWER_LOGIN: "maoawa", + REVIEWER_COAUTHOR: "maoawa <85821597+maoawa@users.noreply.github.com>", +}; +const head = "a".repeat(40), + base = "b".repeat(40); +const record = { + owner: { username: "alice", email: "alice@example.com" }, + domain: "luna", + records: { A: ["1.2.3.4"] }, + proxied: false, +}; +const pr = () => ({ + number: 7, + state: "open", + draft: false, + changed_files: 1, + commits: 1, + comments: 0, + mergeable: true, + mergeable_state: "clean", + title: "", + body: "My blog", + user: { login: "alice" }, + head: { sha: head }, + base: { sha: base, ref: "main" }, +}); +let mockFetch; +let sendMail; +let currentPr; +let writes; +const bindings = () => ({ ...env, ...config, EMAIL: { send: sendMail } }); +const snapshot = () => ({ + pr: pr(), + files: [ + { + filename: "records/luna.json", + status: "added", + text: JSON.stringify(record), + }, + ], + comments: [{ user: { login: "alice" }, body: "Please review my blog." }], +}); +const json = (value) => + new Response(JSON.stringify(value), { + headers: { "Content-Type": "application/json" }, + }); +async function reply(url, options = {}) { + const parsed = new URL(url); + const path = parsed.pathname.replace("/repos/moepage/subdomain", ""); + if (parsed.pathname === "/graphql") + return json({ + data: { + repository: { + record0: { + text: JSON.stringify(record), + byteSize: 100, + isBinary: false, + }, + }, + }, + }); + if (options.method && options.method !== "GET") { + writes.push({ path, ...options }); + if (path.endsWith("/merge")) return json({ merged: true }); + return json({ id: "ok" }); + } + if (path === "/pulls/7") return json(currentPr); + if (path === "/pulls/7/files") + return json([{ filename: "records/luna.json", status: "added" }]); + if (path === "/pulls/7/commits") + return json([ + { + sha: head, + parents: [{}], + commit: { + message: "Create luna.json", + author: { name: "Alice", email: "alice@example.com" }, + }, + }, + ]); + if (path === `/git/trees/${base}`) return json({ tree: [] }); + if (path === `/git/trees/${head}`) + return json({ + tree: [ + { + path: "records/luna.json", + mode: "100644", + sha: "c".repeat(40), + size: 100, + type: "blob", + }, + ], + }); + if (path.startsWith("/contents/")) + return json({ + type: "file", + size: 100, + encoding: "base64", + content: Buffer.from(JSON.stringify(record)).toString("base64"), + }); + if (path.endsWith("/status")) + return json({ + state: "success", + statuses: [{ context: "submission-format", state: "success" }], + }); + if (path.endsWith("/check-runs")) + return json({ + total_count: 1, + check_runs: [{ status: "completed", conclusion: "success" }], + }); + if (path === "/issues/7/comments") + return json([ + { + user: { login: "alice", type: "User" }, + body: "Please review my blog.", + }, + ]); + throw Error(`Unexpected request: ${path}`); +} +beforeEach(async () => { + await env.DB.exec(migration.replace(/\n/g, " ")); + currentPr = pr(); + writes = []; + sendMail = vi.fn(async () => ({ messageId: "test-message" })); + mockFetch = vi.fn(reply); + vi.stubGlobal("fetch", mockFetch); +}); +afterEach(async () => { + vi.unstubAllGlobals(); + await env.DB.exec("DROP TABLE reviews"); +}); +async function seeded(expires = Math.floor(Date.now() / 1000) + 3600) { + const row = { id: crypto.randomUUID(), expires }; + await env.DB.prepare( + "INSERT INTO reviews (id, repository, pr_number, head, base, expires, snapshot) VALUES (?, ?, ?, ?, ?, ?, ?)", + ) + .bind( + row.id, + config.GITHUB_REPOSITORY, + 7, + head, + base, + expires, + JSON.stringify(snapshot()), + ) + .run(); + return { ...row, token: await tokenFor(row, config.REVIEW_LINK_SECRET) }; +} +const get = (token) => + worker.fetch( + new Request(`${config.PUBLIC_URL}/review?token=${token}`), + bindings(), + ); +const post = (token, action, more = {}, headers = {}) => + worker.fetch( + new Request(`${config.PUBLIC_URL}/decision`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Origin: config.PUBLIC_URL, + ...headers, + }, + body: new URLSearchParams({ token, action, ...more }), + }), + bindings(), + ); +async function notification(signatureOverride) { + const body = JSON.stringify({ + repository: config.GITHUB_REPOSITORY, + number: 7, + head, + base, + }); + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = + signatureOverride ?? + (await sign(config.REVIEW_WEBHOOK_SECRET, `${timestamp}.${body}`)); + return worker.fetch( + new Request(`${config.PUBLIC_URL}/notify`, { + method: "POST", + headers: { + "X-Review-Timestamp": timestamp, + "X-Review-Signature": signature, + }, + body, + }), + bindings(), + ); +} +test("GET repeats escaped submission details and never changes GitHub", async () => { + const { token } = await seeded(); + const response = await get(token); + const html = await response.text(); + expect(response.status).toBe(200); + expect(html).toContain("alice@example.com"); + expect(html).toContain("luna.moe.page"); + expect(html).toContain("Please review my blog."); + expect(html).toContain("Approve & merge"); + expect(html).not.toContain("