diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..d7c4c6287d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +please refer to the AGENTS.md file at the root of the repository. diff --git a/.github/scripts/verify-chains.mjs b/.github/scripts/verify-chains.mjs index c2298c40e7..a0379f486a 100644 --- a/.github/scripts/verify-chains.mjs +++ b/.github/scripts/verify-chains.mjs @@ -1,12 +1,12 @@ import fs from 'fs-extra'; -import path from 'path'; +import path from 'node:path'; const DataDirectory = './chains'; const IndexName = 'index.json'; function validate(directory) { let allValid = true; - for (let name of fs.readdirSync(directory)) { + for (const name of fs.readdirSync(directory)) { if (name.startsWith('.') || name === IndexName || name === 'node_modules') continue; const file = path.join(directory, name); const stat = fs.lstatSync(file); @@ -29,11 +29,11 @@ function validate(directory) { } else { const svgValue = fs.readFileSync(path.join(file, 'logo.svg')); if ( - svgValue.includes(`data:image/png;base64`) || - svgValue.includes(`data:img/png;base64`) || - svgValue.includes(`data:image/jpeg;base64`) || - svgValue.includes(`data:img/jpeg;base64`) || - svgValue.includes(`href="http`) + svgValue.includes('data:image/png;base64') || + svgValue.includes('data:img/png;base64') || + svgValue.includes('data:image/jpeg;base64') || + svgValue.includes('data:img/jpeg;base64') || + svgValue.includes('href="http') ) { console.error(`Error: "${file}" logo.svg contains base64 encoded image.`); allValid = false; diff --git a/.gitignore b/.gitignore index 8bd6b0e1a3..ce7acfe844 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -# Ignore scripts folder -scripts/ + .DS_Store node_modules .vscode @@ -14,9 +13,9 @@ _config/nodeAPI/public/137 _config/nodeAPI/public/250 _config/nodeAPI/public/8453 _config/nodeAPI/public/42161 -app/image-tools/dist -app/image-tools/dist/* -app/image-tools/node_modules +app/dist +app/dist/* +app/node_modules # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # misc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 50715a45d6..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "singleQuote": true, - "semi": true, - "useTabs": true, - "tabWidth": 4, - "trailingComma": "none", - "bracketSpacing": false, - "arrowParens": "avoid", - "bracketSameLine": true, - "singleAttributePerLine": true, - "printWidth": 120 -} diff --git a/AGENTS-workflow-template.md b/AGENTS-workflow-template.md new file mode 100644 index 0000000000..0163348d1e --- /dev/null +++ b/AGENTS-workflow-template.md @@ -0,0 +1,351 @@ +# Agent Workflow Template + +The full documentation for OpenAI's Codex coding agents can be found at ~/code/codex/docs (update with your local reference). + +## Worktree-Based Collaboration Workflow + +### Roles + +- **Coordinating/Planning Agent** – runs the Codex MCP server, provisions task/review agents, manages integration branches, and keeps planning docs in sync. +- **Task Agents** – implement scoped changes inside their assigned worktrees, run the necessary validations, and update task documentation. +- **Review Agent(s)** – perform focused reviews from a clean worktree, verify validations, and gate merges. + +### Placeholder Guide + +| Placeholder | Description | +| --- | --- | +| `` | Absolute path to the repository root that hosts the `main` worktree | +| `` | Directory that tracks the default branch (commonly `main/`) | +| `` | Branch that coordinates a wave of tasks | +| `` | Parent directory that stores coordination/task/review worktrees | +| `` | Worktree path dedicated to coordination duties (e.g., `/coordinator`) | +| `` | Branch dedicated to a specific task | +| `` | Worktree path assigned to an individual task agent (e.g., `/task-`) | +| `` | Worktree path used by a review agent (e.g., `/quality-control`) | +| `` | Documentation file that records assignments and status | +| `` | MCP sandbox mode (e.g., `workspace-write`) | +| `` | MCP approval policy (e.g., `on-request`) | +| `` | Placeholder for the project's validation scripts or commands | + +> **Sandbox reminder:** When the harness restricts network access, rerun required remote commands (for example, `git fetch`) with the MCP escalation flag and a brief justification so the coordinator can obtain approval. + +> **Worktree root:** Choose a dedicated parent folder (for example, `/worktrees/`) to host all agent worktrees so they stay isolated from the primary checkout. + +### Coordinator Setup + +- [ ] **Create and prepare the integration branch** for the current wave of tasks: + + ```bash + cd / + git fetch --all --prune + git checkout -b + git push -u origin + ``` + +- [ ] **Create a dedicated coordinator worktree** on the integration branch to avoid conflicts with personal development work: + + ```bash + mkdir -p + git worktree add + cd + ``` + +- [ ] **Instantiate the review tracker** (if it does not exist yet) by copying `docs/02-APP-project-hardening/templates/review-tracker-template.md` into `` and seeding the task queue from `docs/02-APP-project-hardening/overview.md` so coordination starts with an accurate backlog snapshot. + +- [ ] **Launch a Codex MCP server session** the coordinator can call: + + ```bash + codex mcp --sandbox --approval-policy + ``` + + Use the MCP Inspector (or your preferred client) to confirm that the `codex` and `codex-reply` tools are available so new agents can be spawned on demand. + +- [ ] **Create named worktrees** for each task agent on their respective feature branches: + + ```bash + mkdir -p + git worktree add + ``` + + Repeat for each task you plan to run in parallel. Keep the `` checked out on the default branch for syncing upstream or emergency fixes. + +- [ ] **Record worktree paths, assigned agents, and MCP `conversationId`s** in `` so everyone knows where to work. + +- [ ] **Refresh remotes** before assigning work: run `git fetch --all --prune` from ``. + +### Starting Task Agents via MCP + +- [ ] **Create a task session:** call the MCP `codex` tool with a focused prompt, matching sandbox settings, and the prepared worktree path. + +```bash +codex mcp call codex <<'JSON' +{ + "prompt": "You are the Task Agent responsible for . Work exclusively inside , follow the task brief in , and report progress back to the coordinator.", + "sandbox": "", + "approval-policy": "", + "cwd": "", + "include-plan-tool": true +} +JSON +``` + +- [ ] **Store session metadata:** record the returned `conversationId` in `` next to the agent and worktree assignment so you can resume the conversation via the `codex-reply` tool. +- [ ] **Follow-up when needed:** call `codex mcp call codex-reply` with the stored `conversationId` for status checks, escalations, or clarifications. + +### Task Agent Flow + +1. `cd` into the assigned ``. +2. Pull the latest changes with `git pull --ff-only` to stay aligned with other agents working on the same branch. +3. Review the brief and related documentation referenced in ``. +4. Implement the task, keeping scope limited to the brief; update relevant docs/checklists. +5. Run the validations required for the task (formatting, linting, unit/integration tests, builds). Replace `` with your project's scripts. +6. Commit with a conventional message appropriate for the task. +7. Push upstream and document completion in ``. + +### Review Agent Flow + +1. Create a dedicated review worktree on the branch being reviewed: + + ```bash + git worktree add + ``` + +2. Pull the latest changes, run the validation suite, and review diffs (`git diff origin/...HEAD`). +3. Leave review notes in the task document, PR, or tracker, tagging follow-ups for task agents as needed. +4. Once approved, coordinate with the maintainer to merge the reviewed branch into `` (or directly into the target branch, per plan). +5. Remove stale review worktrees with `git worktree remove ` after merge. + +### General Tips + +- Each worktree can only have one branch checked out; name folders clearly to make coordination easier. +- Always fetch/prune from `` so every worktree sees updated refs. +- Use `git worktree list` to audit active worktrees and remove unused ones to avoid stale state. +- Share scripts/configuration via the repository (not per-worktree) so validation commands behave consistently for all agents. + +## Detailed Step-by-Step Agent Workflows + +The sections below provide command-oriented references. Replace placeholders with your project-specific values before running the commands. + +### Coordinating/Planning Agent Workflow + +#### Initial Setup Phase + +```bash +# 1. Navigate to the primary worktree +cd / + +# 2. Ensure clean state and latest upstream +git fetch --all --prune +git checkout +git pull --ff-only + +# 3. Create or update the integration branch +git checkout -b # omit -b if branch already exists +git push -u origin + +# 4. Create coordinator worktree on integration branch +git worktree add +cd + +# 5. Create task worktrees +git worktree add +git worktree add + +# 6. Create or update the task tracker +touch + +# 7. Record worktree assignments in the tracker +# e.g., echo "- Coordinator: ()" >> + +# 8. Commit and push tracker updates as needed +git add +git commit -m "chore: update task assignments" +git push +``` + +#### Ongoing Coordination + +```bash +# Monitor worktree status +git worktree list + +# Sync all worktrees with upstream +git fetch --all --prune + +# Review task completion status +git log --oneline --graph --all + +# Update task assignments +$EDITOR +git add +git commit -m "chore: update task assignments" +git push +``` + +### Task Agent Workflow + +#### Initial Assignment + +```bash +# 1. Navigate to assigned worktree +cd + +# 2. Ensure latest state +git fetch --all --prune +git pull --ff-only + +# 3. Verify current branch and status +git status +git branch -v + +# 4. Review task documentation +cat +``` + +#### Implementation Phase + +```bash +# 1. Implement changes according to the task brief +# (Edit the relevant files) + +# 2. Run project validations + + +# 3. Optionally run local smoke tests or start dev servers as required by the task + + +# 4. Stage and review changes +git add +git diff --staged +``` + +#### Completion Phase + +```bash +# 1. Commit with conventional message +git commit -m ": " + +# 2. Push to upstream +git push + +# 3. Update task tracker (checklist, notes, links) +# e.g., echo "- [x] completed" >> + +git add +git commit -m "chore: update task tracker" +git push + +# 4. Notify the coordinator via MCP or your team channel +``` + +### Review Agent Workflow + +#### Setup Review Environment + +```bash +# 1. Navigate to the primary worktree +cd / + +# 2. Create fresh review worktree +git fetch --all --prune +git worktree add + +# 3. Navigate to review environment +cd + +# 4. Ensure latest state +git pull --ff-only +``` + +#### Review Process + +```bash +# 1. Run the validation suite required for the branch + + +# 2. Review code changes +git diff origin/...HEAD +git log --oneline origin/..HEAD + +# 3. Check for conflicts or issues +git merge-base origin/ HEAD +git diff --name-only origin/...HEAD + +# 4. Perform any domain-specific file checks (update commands as needed) + +``` + +#### Approval & Cleanup + +```bash +# 1. Document review results in the tracker or PR notes +# e.g., echo "## Review Results - " >> + +# 2. Approve for merge when criteria are met +git add +git commit -m "chore: document review results" +git push + +# 3. Navigate back to primary worktree +cd / + +# 4. Merge the reviewed branch into the integration branch or target branch +git checkout +git pull --ff-only +git merge --no-ff +git push + +# 5. Clean up review worktree +git worktree remove + +# 6. Optional: remove feature branch when no longer needed +git branch -d +git push origin --delete +``` + +## Quick Reference Commands + +### Worktree Management + +```bash +# List all worktrees +git worktree list + +# Add new worktree +git worktree add + +# Remove worktree +git worktree remove + +# Prune stale worktree references +git worktree prune +``` + +### Validation Checklist + +Document the commands your project relies on for validation so every agent runs the same checks. + +```bash +# Example placeholders — replace with project-specific scripts + + + + +``` + +### Branch Management + +```bash +# Sync with upstream +git fetch --all --prune + +# Fast-forward pull +git pull --ff-only + +# Check branch status +git status +git branch -v + +# View commit history +git log --oneline --graph +``` diff --git a/AGENTS-workflow.md b/AGENTS-workflow.md new file mode 100644 index 0000000000..be5860b86d --- /dev/null +++ b/AGENTS-workflow.md @@ -0,0 +1,353 @@ +# Agent Workflow (tokenAssets) + +The full documentation for OpenAI's Codex coding agents can be found at ~/code/codex/docs (update with your local reference). + +## Worktree-Based Collaboration Workflow + +### Roles + +- **Coordinating/Planning Agent** – runs the Codex MCP server, provisions task/review agents, manages integration branches, and keeps planning docs in sync. +- **Task Agents** – implement scoped changes inside their assigned worktrees, run the necessary validations, and update task documentation. +- **Review Agent(s)** – perform focused reviews from a clean worktree, verify validations, and gate merges. + +### Placeholder Guide + +| Placeholder | TokenAssets value | Description | +| --- | --- | --- | +| `` | `./main` | Repo directory that contains the `.git` folder | +| `` | `main/` | Default worktree used for day-to-day development | +| `` | `chore/project-hardening` | Branch that aggregates the current wave of work | +| `` | `../worktrees` | Parent directory that stores coordination/task/review worktrees | +| `` | `../worktrees/coordinator` | Persistent worktree dedicated to coordination duties | +| `` | `task/` | Branch dedicated to a specific task | +| `` | `../worktrees/task-` | Worktree assigned to an individual task agent | +| `` | `../worktrees/quality-control` (shared) or `../worktrees/review-` | Clean worktree used by a review agent | +| `` | `docs/02-APP-project-hardening/review-tracker.md` | Tracker that records assignments and status | +| `` | `workspace-write` | MCP sandbox mode for Codex sessions | +| `` | `on-request` | Approval policy for Codex sessions | +| `` | See `package.json` scripts | Project validation commands (lint, test, build) | + +> **Template reference:** For a generalized process, use `AGENTS-workflow-template.md`. This file captures the tokenAssets-specific defaults. + +> **Repo layout note:** The actual Git repository lives in the `main/` subdirectory (`` = `./main`). The parent `tokenAssets/` folder only groups worktrees, so run git-oriented commands from `main/` or the sibling worktrees the scripts create. + +> **Branch naming note:** `main` stays as the default branch, shared integration work runs on `chore/project-hardening`, and each task agent works on a `task/` branch (for example, `task/upload-api-hardening`). Keep persistent worktrees under `` (e.g., `coordinator`, `quality-control`, `task-`). The shared CLI (“shell” folder) sits one level above `main/`, so coordinator commands run from `./main` while pointing at the appropriate `/*` directory when issuing `git` or validation commands. + +> **Sandbox reminder:** When the harness restricts network access, rerun required remote commands (e.g., `git fetch`) with `with_escalated_permissions: true` and a short justification so the approval prompt appears. + +### Coordinator Setup + +- [ ] **Create and prepare the integration branch** for the current wave of tasks: + + ```bash + cd / + git fetch --all --prune + git checkout -b + git push -u origin + ``` + +- [ ] **Create a dedicated coordinator worktree** on the integration branch to avoid conflicts with personal development work: + + ```bash + mkdir -p + git worktree add + cd + ``` + +- [ ] **Launch a Codex MCP server session** the coordinator can call: + + ```bash + codex mcp --sandbox --approval-policy + ``` + + Use the MCP Inspector (or your preferred client) to confirm that the `codex` and `codex-reply` tools are available so new agents can be spawned on demand. + +- [ ] **Create named worktrees** for each task agent on their respective feature branches: + + ```bash + mkdir -p + git worktree add + ``` + + Repeat for each task you plan to run in parallel. Keep the `` checked out on the default branch for syncing upstream or emergency fixes. + +- [ ] **Record worktree paths, assigned agents, and MCP `conversationId`s** in `` so everyone knows where to work. + +- [ ] **Refresh remotes** before assigning work: run `git fetch --all --prune` from ``. + +### Starting Task Agents via MCP + +- [ ] **Create a task session:** call the MCP `codex` tool with a focused prompt, matching sandbox settings, and the prepared worktree path. + +```bash +codex mcp call codex <<'JSON' +{ + "prompt": "You are the Task Agent responsible for . Work exclusively inside , follow the task brief in , and report progress back to the coordinator.", + "sandbox": "", + "approval-policy": "", + "cwd": "", + "include-plan-tool": true +} +JSON +``` + +- [ ] **Store session metadata:** record the returned `conversationId` in `` next to the agent and worktree assignment so you can resume the conversation via the `codex-reply` tool. +- [ ] **Follow-up when needed:** call `codex mcp call codex-reply` with the stored `conversationId` for status checks, escalations, or clarifications. + +### Task Agent Flow + +1. `cd` into the assigned ``. +2. Pull the latest changes with `git pull --ff-only` to stay aligned with other agents working on the same branch. +3. Review the brief and related documentation referenced in ``. +4. Implement the task, keeping scope limited to the brief; update relevant docs/checklists. +5. Run the validations required for the task (formatting, linting, unit/integration tests, builds). Replace `` with your project's scripts. +6. Commit with a conventional message appropriate for the task. +7. Push upstream and document completion in ``. + +### Review Agent Flow + +1. Create a dedicated review worktree on the branch being reviewed: + + ```bash + git worktree add + ``` + +2. Pull the latest changes, run the validation suite, and review diffs (`git diff origin/...HEAD`). +3. Leave review notes in the task document, PR, or tracker, tagging follow-ups for task agents as needed. +4. Once approved, coordinate with the maintainer to merge the reviewed branch into `` (or directly into the target branch, per plan). +5. Remove stale review worktrees with `git worktree remove ` after merge. + +### General Tips + +- Each worktree can only have one branch checked out; name folders clearly to make coordination easier. +- Always fetch/prune from `` so every worktree sees updated refs. +- Use `git worktree list` to audit active worktrees and remove unused ones to avoid stale state. +- Share scripts/configuration via the repository (not per-worktree) so validation commands behave consistently for all agents. + +## Detailed Step-by-Step Agent Workflows + +The sections below provide command-oriented references. Replace placeholders with your project-specific values before running the commands. + +### Coordinating/Planning Agent Workflow + +#### Initial Setup Phase + +```bash +# 1. Navigate to the primary worktree +cd / + +# 2. Ensure clean state and latest upstream +git fetch --all --prune +git checkout +git pull --ff-only + +# 3. Create or update the integration branch +git checkout -b # omit -b if branch already exists +git push -u origin + +# 4. Create coordinator worktree on integration branch +git worktree add +cd + +# 5. Create task worktrees +git worktree add +git worktree add + +# 6. Create or update the task tracker +touch + +# 7. Record worktree assignments in the tracker +# e.g., echo "- Coordinator: ()" >> + +# 8. Commit and push tracker updates as needed +git add +git commit -m "chore: update task assignments" +git push +``` + +#### Ongoing Coordination + +```bash +# Monitor worktree status +git worktree list + +# Sync all worktrees with upstream +git fetch --all --prune + +# Review task completion status +git log --oneline --graph --all + +# Update task assignments +$EDITOR +git add +git commit -m "chore: update task assignments" +git push +``` + +### Task Agent Workflow + +#### Initial Assignment + +```bash +# 1. Navigate to assigned worktree +cd + +# 2. Ensure latest state +git fetch --all --prune +git pull --ff-only + +# 3. Verify current branch and status +git status +git branch -v + +# 4. Review task documentation +cat +``` + +#### Implementation Phase + +```bash +# 1. Implement changes according to the task brief +# (Edit the relevant files) + +# 2. Run project validations + + +# 3. Optionally run local smoke tests or start dev servers as required by the task + + +# 4. Stage and review changes +git add +git diff --staged +``` + +#### Completion Phase + +```bash +# 1. Commit with conventional message +git commit -m ": " + +# 2. Push to upstream +git push + +# 3. Update task tracker (checklist, notes, links) +# e.g., echo "- [x] completed" >> + +git add +git commit -m "chore: update task tracker" +git push + +# 4. Notify the coordinator via MCP or your team channel +``` + +### Review Agent Workflow + +#### Setup Review Environment + +```bash +# 1. Navigate to the primary worktree +cd / + +# 2. Create fresh review worktree +git fetch --all --prune +git worktree add + +# 3. Navigate to review environment +cd + +# 4. Ensure latest state +git pull --ff-only +``` + +#### Review Process + +```bash +# 1. Run the validation suite required for the branch + + +# 2. Review code changes +git diff origin/...HEAD +git log --oneline origin/..HEAD + +# 3. Check for conflicts or issues +git merge-base origin/ HEAD +git diff --name-only origin/...HEAD + +# 4. Perform any domain-specific file checks (update commands as needed) + +``` + +#### Approval & Cleanup + +```bash +# 1. Document review results in the tracker or PR notes +# e.g., echo "## Review Results - " >> + +# 2. Approve for merge when criteria are met +git add +git commit -m "chore: document review results" +git push + +# 3. Navigate back to primary worktree +cd / + +# 4. Merge the reviewed branch into the integration branch or target branch +git checkout +git pull --ff-only +git merge --no-ff +git push + +# 5. Clean up review worktree +git worktree remove + +# 6. Optional: remove feature branch when no longer needed +git branch -d +git push origin --delete +``` + +## Quick Reference Commands + +### Worktree Management + +```bash +# List all worktrees +git worktree list + +# Add new worktree +git worktree add + +# Remove worktree +git worktree remove + +# Prune stale worktree references +git worktree prune +``` + +### Validation Checklist + +Document the commands your project relies on for validation so every agent runs the same checks. + +```bash +# Example placeholders — replace with project-specific scripts + + + + +``` + +### Branch Management + +```bash +# Sync with upstream +git fetch --all --prune + +# Fast-forward pull +git pull --ff-only + +# Check branch status +git status +git branch -v + +# View commit history +git log --oneline --graph +``` diff --git a/AGENTS.md b/AGENTS.md index 42a7faa62e..99a21f5138 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ There are multiple different apps and elements in this repo. Please read careful - Source assets: `tokens//
/` with `logo.svg`, `logo-32.png`, `logo-128.png`. - Chain assets: `chains//` -- Image Upload App: `app/image-tools/` contains a repo where users can upload token logos to the repo. It is wholly unrelated to anything in the `_config/` folder and when working on this app, you should ignore the _config folder and its contents. +- Image Upload App: `app/` contains the upload tooling where users can submit token logos to the repo. It is wholly unrelated to anything in the `_config/` folder and when working on this app, you should ignore the \_config folder and its contents. - Deprecated APIs: `_config/`, `_config/nodeAPI`, and `_config/goAPI` contain legacy code for APIs to serve the token logos that we do not actively use but still support for legacy applications. Generally they should be ignored unless explicitly requested to work on them. - Automation: `scripts/` (e.g., `ingestTokens.js` and image inputs under `scripts/token-images-to-ingest/`). - Root configs: `.editorconfig`, `.prettierrc`, `package.json`. @@ -19,7 +19,10 @@ There are multiple different apps and elements in this repo. Please read careful - Check format: `yarn format:check` — verifies formatting without writing. - Next.js dev (API): `yarn --cwd _config/nodeAPI dev` — starts the local API for previewing assets. - Next.js build: `yarn --cwd _config/nodeAPI build` — type-checks and builds the API bundle. -- Ingest assets: `node scripts/ingestTokens.js ./scripts/tokensToInjest.json` — copies/renames prepared images into `tokens/`. +- Image tools dev: `bun dev` in `app` (Vite on `http://localhost:5173`). +- Image tools serverless preview: `vercel dev` in `app` (serves `/api/*`). +- Image tools build/preview: `bun build` then `bun preview` in `app`. +- Image tools lint/typecheck/tests: `bun lint`, `bun typecheck`, `bun run test` (Vitest) — use `bun run validate` to run all three. (`bun test` invokes Bun's experimental runner and will fail on our Vitest helpers.) ## Coding Style & Naming Conventions @@ -35,6 +38,11 @@ There are multiple different apps and elements in this repo. Please read careful - Running `yarn --cwd _config/nodeAPI dev` and fetching `/api/token//
/logo-32.png`. - Ensuring both PNG sizes exist and load; prefer PNG for production. - Running `yarn format:check` and `yarn --cwd _config/nodeAPI lint` when editing `_config/nodeAPI`. +- For `app/`, validate via `vercel dev`: + - OAuth callback: `/api/auth/github/callback` returns to `/auth/github/success`. + - ERC-20 name lookup: POST `/api/erc20-name` (Edge). + - Upload + PR: POST `/api/upload` (Edge) and confirm the returned PR URL. +- Optional: enable `scripts/git-hooks/pre-commit` (copy from `.sample`) via `git config core.hooksPath scripts/git-hooks` to run lint/typecheck/tests before committing. ## Commit & Pull Request Guidelines @@ -48,3 +56,49 @@ There are multiple different apps and elements in this repo. Please read careful - Optimize SVGs (keep simple; large/complex SVGs hinder performance). - Ensure PNGs are exactly 32×32 and 128×128. - Do not commit secrets or binaries outside `tokens/` and `_config/` build outputs. + +## Branching Guidance + +Default to one shared integration branch per wave (e.g., `wave-1/shared-utilities`) so agents working the same wave commit directly together and stay aligned on helper contracts. Only spin up individual branches for isolated or risky spikes; otherwise per-agent branches cause avoidable rebases and drift. + +## Worktree-Based Collaboration Workflow + +### Roles + +- **Coordinating/Planning Agent** – sets up integration branches, allocates tasks, and keeps the tracker up to date. +- **Task Agents** – implement scoped changes inside their assigned worktrees, run validations, and update task docs. +- **Review Agent(s)** – perform focused reviews from a clean worktree, verify validations, and gate merges. + +### Coordinator Setup + +1. Pick/prepare the integration branch (e.g., `wave-1/shared-utilities`) and push it upstream. +2. Create named worktrees for each active branch: + - `git worktree add ../wave1 task/shared-utilities-alignment` + - `git worktree add ../wave1-devex task/developer-experience-upgrades` + - Keep a root worktree on `main` for syncing upstream or emergency fixes. +3. Record worktree paths plus assigned agents in `docs/project-hardening/review-tracker.md` so everyone knows where to work. +4. Before assignments, run `git fetch --all --prune` from the main repo to keep every worktree in sync. + +### Task Agent Flow + +1. `cd` into the assigned worktree (e.g., `../wave1`). +2. Pull latest changes with `git pull --ff-only` to stay aligned with other agents on the same branch. +3. Implement the task, keeping scope limited to the brief; update relevant docs/checklists there. +4. Run required validations (typecheck, build, tests) from the same directory. +5. Commit with a conventional message (e.g., `chore: align shared utilities`). +6. Push upstream and note completion in the task document and tracker. + +### Review Agent Flow + +1. Create a dedicated review worktree: `git worktree add ../wave1-review task/shared-utilities-alignment`. +2. Pull latest, run the validation suite, and review diffs (`git diff origin/main...HEAD`). +3. Leave review notes in the task doc or PR, tagging follow-ups for task agents. +4. Once approved, coordinate with the maintainer to merge the shared branch into the integration branch (or directly into `improvement-review-implementation`, per plan). +5. Remove stale review worktrees with `git worktree remove ../wave1-review` after merge. + +### General Tips + +- Each worktree can only have one branch checked out; name folders clearly (`../waveX`, `../waveX-review`, etc.). +- Always fetch/prune from the main repo directory (`tokenAssets/`) so every worktree sees updated refs. +- Use `git worktree list` to audit active worktrees; remove unused ones to avoid stale state. +- Share scripts/configs via the repo (not per-worktree) so validation commands behave consistently. diff --git a/README.md b/README.md index 4469184fc2..18e3ec1ffb 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,13 @@ Node Version: 18.x Environment Variables: None ``` +## Tooling & Validation + +- `bun lint` — ESLint (React + TypeScript). +- `bun typecheck` — TypeScript project check. +- `bun test` — Vitest unit tests for shared helpers. +- `bun run validate` — Run lint, typecheck, and unit tests together. + ## Supported chains - 1: Ethereum diff --git a/app/image-tools/.env.local.example b/app/.env.local.example similarity index 74% rename from app/image-tools/.env.local.example rename to app/.env.local.example index 334f8c25a0..f8208e551f 100644 --- a/app/image-tools/.env.local.example +++ b/app/.env.local.example @@ -1,6 +1,6 @@ # Frontend (Vite) – only VITE_* vars are exposed to the browser VITE_GITHUB_CLIENT_ID=your_github_oauth_app_client_id -VITE_API_BASE_URL=http://localhost:5174 +# VITE_API_BASE_URL=http://localhost:5174 - not needed, inferred from request headers VITE_RPC_URI_FOR_1=https://your_rpc_provider_for_chain_1 VITE_RPC_URI_FOR_10=https://your_rpc_provider_for_chain_10 VITE_RPC_URI_FOR_100=https://your_rpc_provider_for_chain_100 @@ -14,9 +14,11 @@ VITE_RPC_URI_FOR_747474=https://your_rpc_provider_for_chain_747474 # Server (Express) GITHUB_CLIENT_ID=your_github_oauth_app_client_id GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret -APP_BASE_URL=http://localhost:5173 -API_BASE_URL=http://localhost:5174 +# APP_BASE_URL=http://localhost:5173 - not needed, inferred from request headers +API_BASE_URL=set_to_prod_site_URL REPO_OWNER=your-github-username-or-org REPO_NAME=tokenAssets # Optional: override API port # PORT=5174 +# Optional: for custom deployment domains (Vercel auto-sets VERCEL_URL) +# VERCEL_URL=your-custom-domain.com diff --git a/app/image-tools/AGENTS.md b/app/AGENTS.md similarity index 67% rename from app/image-tools/AGENTS.md rename to app/AGENTS.md index ab43de7a5d..efdf02049a 100644 --- a/app/image-tools/AGENTS.md +++ b/app/AGENTS.md @@ -4,15 +4,19 @@ - Assets: `tokens//
/` with `logo.svg`, `logo-32.png`, `logo-128.png`. - Chains: `chains//` (numeric `chainId`). -- Image Upload App: `app/image-tools/`. +- Image Upload App: `app/` (SPA + Vercel edge functions). +- Shared helpers: `app/src/shared/` (import via `@shared/*` from both SPA and edge code). - Automation: `scripts/` (e.g., `ingestTokens.js`; inputs in `scripts/token-images-to-ingest/`). - Root configs: `.editorconfig`, `.prettierrc`, `package.json`. ## Build, Test, and Development Commands -- SPA dev: `bun dev` in `app/image-tools` (Vite on `http://localhost:5173`). -- Vercel dev: `vercel dev` in `app/image-tools` (serves API under `/api/*`). +- SPA dev: `bun dev` in `app` (Vite on `http://localhost:5173`). +- Vercel dev: `vercel dev` in `app` (serves API under `/api/*`). - Build/preview: `bun build` then `bun preview`. +- Type/lint: `bun run lint` or `bun run typecheck` (TS only). +- Tests: `bun run test` (Vitest, single-threaded; covers `@shared` helpers plus `/api/erc20-name`). +- Full sweep: `bun run validate` (`lint → typecheck → test`). - Ingest assets: `node scripts/ingestTokens.js ./scripts/tokensToInjest.json` — copies prepared images into `tokens/`. ## Coding Style & Naming Conventions @@ -25,11 +29,13 @@ ## Testing Guidelines -- No formal test suite. Validate via Vercel dev: +- Unit tests: `bun run test` covers shared helpers (ABI decode, RPC selection, API base builders) and the `/api/erc20-name` edge handler (caching, error codes, timeout). +- Integration smoke: run `vercel dev` and validate: - OAuth callback: `/api/auth/github/callback` returns to `/auth/github/success`. - ERC-20 name lookup: POST `/api/erc20-name` (Edge). - Upload + PR: POST `/api/upload` (Edge) and confirm PR URL. - Ensure PNGs are exactly 32×32 and 128×128; keep SVGs optimized. +- Configure ERC-20 lookup caching/timeouts via `ERC20_NAME_CACHE_TTL_MS`, `ERC20_NAME_CACHE_SIZE`, `ERC20_NAME_RPC_TIMEOUT_MS` when deploying edge functions. ## Commit & Pull Request Guidelines diff --git a/app/image-tools/README.md b/app/README.md similarity index 58% rename from app/image-tools/README.md rename to app/README.md index 89344552ee..107b4cdb10 100644 --- a/app/image-tools/README.md +++ b/app/README.md @@ -13,6 +13,9 @@ A lightweight SPA + Vercel Functions app for uploading token/chain assets and op - `APP_BASE_URL` — optional; default request origin. Only set if SPA and API are on different origins. - `REPO_OWNER` (default `yearn`), `REPO_NAME` (default `tokenAssets`). - `ALLOW_REPO_OVERRIDE` — set to `true` only if you intentionally want to target a non-yearn repo when deploying from a fork. + - `ERC20_NAME_CACHE_TTL_MS` — optional; TTL for `/api/erc20-name` cache entries (default 5 minutes). + - `ERC20_NAME_CACHE_SIZE` — optional; max cached entries for `/api/erc20-name` (default 256 entries). + - `ERC20_NAME_RPC_TIMEOUT_MS` — optional; abort RPC requests after this many milliseconds (default 10 seconds). - GitHub OAuth App callback must match the current domain: `https:///api/auth/github/callback` (or `http://localhost:3000/...` for `vercel dev`). ## Commands @@ -20,14 +23,16 @@ A lightweight SPA + Vercel Functions app for uploading token/chain assets and op - `bun dev` — Vite dev server for the SPA (http://localhost:5173). - `vercel dev` — Runs API routes and serves the SPA locally (recommended for full flow). - `bun build` / `bun preview` — Build and preview the SPA. -- `bun typecheck` — TypeScript type checks (acts as lightweight lint). -- `bun lint` — Alias to type checks. +- `bun run lint` — ESLint with React/TypeScript/JSX a11y rules; fails on warnings. +- `bun run typecheck` — TypeScript project checks (`tsc --noEmit`). +- `bun run test` — Vitest unit tests for shared helpers and `/api/erc20-name` (single-threaded, node environment). +- `bun run validate` — Convenience script that runs lint → typecheck → test in sequence. ## App Flow (What Calls What) 1) Open the site — SPA loads; no API calls by default. 2) Sign in with GitHub — Browser goes to GitHub OAuth; upon approval GitHub redirects to `/api/auth/github/callback` (Edge). The function exchanges the code for a token and redirects to `/auth/github/success` where the token is stored. -3) Enter chain/address — Client may call `POST /api/erc20-name` (Edge) to resolve ERC‑20 name. +3) Enter chain/address — Client calls `POST /api/erc20-name` (Edge) to resolve ERC‑20 name. The API responds with `{name, cache: {hit, expiresAt}}`; errors return `{error: {code, message, details?}}` for actionable feedback. 4) Drop SVG — Client generates PNG previews (32×32, 128×128) via Canvas. 5) Submit PR — Client posts multipart form to `POST /api/upload` (Edge) with `svg`, `png32`, and `png128`. The function validates sizes and opens a PR via GitHub API. @@ -35,3 +40,6 @@ A lightweight SPA + Vercel Functions app for uploading token/chain assets and op - PNGs are generated client‑side and validated on the server. - Keep SVGs simple/optimized; ensure PNGs are exactly 32×32 and 128×128. +- Shared utilities (ABI decoding, RPC resolution, API base builders) live under `app/src/shared/` and can be imported via the `@shared/*` alias from both SPA and edge runtime code. +- ERC-20 name lookups are cached in-memory on the edge runtime and use AbortController on both client and server to cancel overlapping requests. +- Optional git hook: copy `scripts/git-hooks/pre-commit.sample` to `scripts/git-hooks/pre-commit` and set `git config core.hooksPath scripts/git-hooks` to enforce lint/typecheck/tests locally. diff --git a/app/api/__tests__/erc20-name.test.ts b/app/api/__tests__/erc20-name.test.ts new file mode 100644 index 0000000000..3f9a5601e6 --- /dev/null +++ b/app/api/__tests__/erc20-name.test.ts @@ -0,0 +1,147 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; + +const VALID_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; +const ENCODED_DYNAMIC_TEST = + '0x' + + '0000000000000000000000000000000000000000000000000000000000000020' + + '0000000000000000000000000000000000000000000000000000000000000004' + + '5465737400000000000000000000000000000000000000000000000000000000'; + +type HandlerModule = typeof import('../erc20-name'); + +declare const Response: typeof globalThis.Response; + +declare const Request: typeof globalThis.Request; + +function makeRequest(body: unknown) { + return new Request('https://example.com/api/erc20-name', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(body) + }); +} + +async function loadHandler(): Promise { + return await import('../erc20-name'); +} + +function setRpcEnv(url?: string) { + if (url) { + process.env.VITE_RPC_URI_FOR_1 = url; + } else { + process.env.VITE_RPC_URI_FOR_1 = undefined; + } +} + +describe('api/erc20-name', () => { + beforeEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + vi.useRealTimers(); + for (const key of Object.keys(process.env)) { + if (key.startsWith('VITE_RPC_') || key.startsWith('ERC20_NAME_')) delete process.env[key]; + } + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + vi.useRealTimers(); + }); + + it('returns name and caches result', async () => { + setRpcEnv('https://rpc.example'); + const mockFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({result: ENCODED_DYNAMIC_TEST}), { + status: 200, + headers: {'Content-Type': 'application/json'} + }) + ); + vi.stubGlobal('fetch', mockFetch); + const mod = await loadHandler(); + const {default: handler, __clearCacheForTesting} = mod; + const first = await handler(makeRequest({chainId: 1, address: VALID_ADDRESS})); + expect(first.status).toBe(200); + const body1 = await first.json(); + expect(body1).toEqual({name: 'Test', cache: {hit: false, expiresAt: expect.any(Number)}}); + expect(mockFetch).toHaveBeenCalledTimes(1); + const second = await handler(makeRequest({chainId: 1, address: VALID_ADDRESS})); + const body2 = await second.json(); + expect(second.status).toBe(200); + expect(body2).toEqual({name: 'Test', cache: {hit: true, expiresAt: expect.any(Number)}}); + expect(mockFetch).toHaveBeenCalledTimes(1); + __clearCacheForTesting(); + }); + + it('returns 400 for invalid address', async () => { + setRpcEnv('https://rpc.example'); + const mockFetch = vi.fn(); + vi.stubGlobal('fetch', mockFetch); + const {default: handler, __clearCacheForTesting} = await loadHandler(); + const res = await handler(makeRequest({chainId: 1, address: 'not-an-address'})); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error.code).toBe('INVALID_ADDRESS'); + expect(mockFetch).not.toHaveBeenCalled(); + __clearCacheForTesting(); + }); + + it('surfaces RPC HTTP errors with details', async () => { + setRpcEnv('https://rpc.example'); + const mockFetch = vi + .fn() + .mockResolvedValue(new Response('Internal error', {status: 500, headers: {'Content-Type': 'text/plain'}})); + vi.stubGlobal('fetch', mockFetch); + const {default: handler, __clearCacheForTesting} = await loadHandler(); + const res = await handler(makeRequest({chainId: 1, address: VALID_ADDRESS})); + const body = await res.json(); + expect(res.status).toBe(502); + expect(body.error.code).toBe('RPC_HTTP_ERROR'); + __clearCacheForTesting(); + }); + + it('handles RPC JSON errors', async () => { + setRpcEnv('https://rpc.example'); + const mockFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({error: {message: 'execution reverted'}}), { + status: 200, + headers: {'Content-Type': 'application/json'} + }) + ); + vi.stubGlobal('fetch', mockFetch); + const {default: handler, __clearCacheForTesting} = await loadHandler(); + const res = await handler(makeRequest({chainId: 1, address: VALID_ADDRESS})); + const body = await res.json(); + expect(res.status).toBe(502); + expect(body.error.code).toBe('RPC_JSON_ERROR'); + __clearCacheForTesting(); + }); + + it('returns error when RPC URL missing', async () => { + setRpcEnv(undefined); + const mockFetch = vi.fn(); + vi.stubGlobal('fetch', mockFetch); + const {default: handler, __clearCacheForTesting} = await loadHandler(); + const res = await handler(makeRequest({chainId: 999999, address: VALID_ADDRESS})); + const body = await res.json(); + expect(res.status).toBe(500); + expect(body.error.code).toBe('RPC_NOT_CONFIGURED'); + expect(mockFetch).not.toHaveBeenCalled(); + __clearCacheForTesting(); + }); + + it('handles aborted RPC requests', async () => { + setRpcEnv('https://rpc.example'); + const abortError = Object.assign(new Error('Aborted'), {name: 'AbortError'}); + const mockFetch = vi.fn().mockRejectedValue(abortError); + vi.stubGlobal('fetch', mockFetch); + const {default: handler, __clearCacheForTesting} = await loadHandler(); + const res = await handler(makeRequest({chainId: 1, address: VALID_ADDRESS})); + const body = await res.json(); + expect(res.status).toBe(502); + expect(body.error.code).toBe('RPC_REQUEST_FAILED'); + expect(body.error.message).toContain('timed out'); + __clearCacheForTesting(); + }); +}); diff --git a/app/api/_lib/upload.test.ts b/app/api/_lib/upload.test.ts new file mode 100644 index 0000000000..80eeccd81e --- /dev/null +++ b/app/api/_lib/upload.test.ts @@ -0,0 +1,253 @@ +import {describe, expect, it} from 'vitest'; + +import {UploadValidationError, buildDefaultPrMetadata, buildPrFiles, parseUploadForm} from './upload'; + +function createSvgBlob(): Blob { + return new Blob([''], {type: 'image/svg+xml'}); +} + +function createPng32Blob(): Blob { + // Create a minimal valid PNG file (32x32 pixel transparent PNG) + const pngBytes = new Uint8Array([ + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, // PNG signature + 0x00, + 0x00, + 0x00, + 0x0d, // IHDR chunk length (13 bytes) + 0x49, + 0x48, + 0x44, + 0x52, // IHDR + 0x00, + 0x00, + 0x00, + 0x20, // Width: 32 pixels + 0x00, + 0x00, + 0x00, + 0x20, // Height: 32 pixels + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, // Bit depth: 8, Color type: 6 (RGBA), Compression: 0, Filter: 0, Interlace: 0 + 0x8d, + 0x6f, + 0x26, + 0x53, // IHDR CRC + 0x00, + 0x00, + 0x00, + 0x0a, // IDAT chunk length (10 bytes) + 0x49, + 0x44, + 0x41, + 0x54, // IDAT + 0x78, + 0x9c, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, // Compressed data (empty 32x32 transparent image) + 0x0d, + 0x0a, + 0x2d, + 0xb4, // IDAT CRC + 0x00, + 0x00, + 0x00, + 0x00, // IEND chunk length (0 bytes) + 0x49, + 0x45, + 0x4e, + 0x44, // IEND + 0xae, + 0x42, + 0x60, + 0x82 // IEND CRC + ]); + return new Blob([pngBytes], {type: 'image/png'}); +} + +function createPng128Blob(): Blob { + // Create a minimal valid PNG file (128x128 pixel transparent PNG) + const pngBytes = new Uint8Array([ + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, // PNG signature + 0x00, + 0x00, + 0x00, + 0x0d, // IHDR chunk length (13 bytes) + 0x49, + 0x48, + 0x44, + 0x52, // IHDR + 0x00, + 0x00, + 0x00, + 0x80, // Width: 128 pixels + 0x00, + 0x00, + 0x00, + 0x80, // Height: 128 pixels + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, // Bit depth: 8, Color type: 6 (RGBA), Compression: 0, Filter: 0, Interlace: 0 + 0xc3, + 0x3e, + 0x61, + 0xcb, // IHDR CRC (updated for 128x128) + 0x00, + 0x00, + 0x00, + 0x0a, // IDAT chunk length (10 bytes) + 0x49, + 0x44, + 0x41, + 0x54, // IDAT + 0x78, + 0x9c, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, // Compressed data (empty 128x128 transparent image) + 0x0d, + 0x0a, + 0x2d, + 0xb4, // IDAT CRC + 0x00, + 0x00, + 0x00, + 0x00, // IEND chunk length (0 bytes) + 0x49, + 0x45, + 0x4e, + 0x44, // IEND + 0xae, + 0x42, + 0x60, + 0x82 // IEND CRC + ]); + return new Blob([pngBytes], {type: 'image/png'}); +} + +function validAddress(index: number): string { + return `0x${index.toString(16).padStart(40, '0')}`; +} + +describe('parseUploadForm', () => { + it('parses a single token submission with expected shape', async () => { + const form = new FormData(); + form.set('target', 'token'); + form.append('address', validAddress(1)); + form.set('chainId', '1'); + form.set('chainId_0', '1'); + form.set('svg_0', createSvgBlob()); + form.set('png32_0', createPng32Blob()); + form.set('png128_0', createPng128Blob()); + + const result = await parseUploadForm(form); + if (result.target !== 'token') throw new Error('expected token upload result'); + expect(result.tokens).toHaveLength(1); + expect(result.tokens[0]).toMatchObject({ + chainId: '1', + address: validAddress(1), + svgBase64: 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==', + png32Base64: 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAACNbyZTAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==', + png128Base64: 'iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==' + }); + }); + + it('throws UploadValidationError with field details for invalid address', async () => { + const form = new FormData(); + form.set('target', 'token'); + form.append('address', 'not-an-address'); + form.set('chainId', '1'); + form.set('chainId_0', '1'); + form.set('svg_0', createSvgBlob()); + form.set('png32_0', createPng32Blob()); + form.set('png128_0', createPng128Blob()); + + const attempt = parseUploadForm(form); + await expect(attempt).rejects.toBeInstanceOf(UploadValidationError); + await expect(attempt).rejects.toMatchObject({ + details: expect.arrayContaining([ + expect.objectContaining({field: 'address', message: 'address must be a valid EVM address'}) + ]) + }); + }); +}); + +describe('buildPrFiles', () => { + it('returns repository paths for token files', () => { + const files = buildPrFiles({ + target: 'token', + tokens: [ + { + index: 0, + chainId: '1', + address: validAddress(1), + svgBase64: 'svg', + png32Base64: 'png32', + png128Base64: 'png128' + } + ], + overrides: {} + }); + + expect(files.map(file => file.path)).toEqual([ + 'tokens/1/0x0000000000000000000000000000000000000001/logo.svg', + 'tokens/1/0x0000000000000000000000000000000000000001/logo-32.png', + 'tokens/1/0x0000000000000000000000000000000000000001/logo-128.png' + ]); + }); +}); + +describe('buildDefaultPrMetadata', () => { + it('generates default metadata when overrides are missing', () => { + const metadata = buildDefaultPrMetadata( + { + target: 'token', + tokens: [ + { + index: 0, + chainId: '1', + address: validAddress(1), + svgBase64: 'svg', + png32Base64: 'png32', + png128Base64: 'png128' + } + ], + overrides: {} + }, + {} + ); + + expect(metadata.title).toContain('feat: add token assets'); + expect(metadata.body).toContain('Chains: 1'); + expect(metadata.body).toContain('/tokens/1/0x0000000000000000000000000000000000000001/logo.svg'); + }); +}); diff --git a/app/api/_lib/upload.ts b/app/api/_lib/upload.ts new file mode 100644 index 0000000000..4239473881 --- /dev/null +++ b/app/api/_lib/upload.ts @@ -0,0 +1,440 @@ +// Inlined EVM utilities +const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/i; + +function isEvmAddress(address: string): boolean { + if (typeof address !== 'string') return false; + return ADDRESS_REGEX.test(address.trim()); +} + +// Inlined image utilities +type PngDimensions = { + width: number; + height: number; +}; + +const PNG_SIGNATURE = Object.freeze([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +function isPng(bytes: Uint8Array): boolean { + if (bytes.length < PNG_SIGNATURE.length) return false; + for (let i = 0; i < PNG_SIGNATURE.length; i++) { + if (bytes[i] !== PNG_SIGNATURE[i]) return false; + } + return true; +} + +function readUInt32BE(bytes: Uint8Array, offset: number): number { + return ( + ((bytes[offset] << 24) >>> 0) + + ((bytes[offset + 1] << 16) >>> 0) + + ((bytes[offset + 2] << 8) >>> 0) + + (bytes[offset + 3] >>> 0) + ); +} + +function getPngDimensions(bytes: Uint8Array): PngDimensions | null { + if (!isPng(bytes)) return null; + if (bytes.length < 24) return null; + const width = readUInt32BE(bytes, 16); + const height = readUInt32BE(bytes, 20); + if (!width || !height) return null; + return {width, height}; +} + +function assertDimensions( + label: string, + dimensions: PngDimensions | null, + expected: {width: number; height: number} +): void { + if (!dimensions) throw new Error(`${label} must be a valid PNG file`); + const {width, height} = dimensions; + if (width !== expected.width || height !== expected.height) { + throw new Error(`${label} must be ${expected.width}x${expected.height} (received ${width}x${height})`); + } +} + +async function blobToUint8(blob: Blob): Promise { + const anyBlob = blob as unknown as { + arrayBuffer?: () => Promise; + stream?: () => ReadableStream; + buffer?: ArrayBufferLike; + }; + if (typeof anyBlob?.arrayBuffer === 'function') { + const buffer = await anyBlob.arrayBuffer(); + return new Uint8Array(buffer); + } + if (typeof anyBlob?.stream === 'function') { + const reader = anyBlob.stream().getReader(); + const chunks: Uint8Array[] = []; + let done = false; + while (!done) { + const result = await reader.read(); + done = Boolean(result.done); + if (result.value) chunks.push(result.value); + } + const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const merged = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + merged.set(chunk, offset); + offset += chunk.length; + } + return merged; + } + if (anyBlob instanceof Uint8Array) return new Uint8Array(anyBlob); + if (anyBlob?.buffer instanceof ArrayBuffer) return new Uint8Array(anyBlob.buffer); + if (typeof Response !== 'undefined') { + try { + const response = new Response(blob); + const buffer = await response.arrayBuffer(); + return new Uint8Array(buffer); + } catch { + // ignore and fall through + } + } + throw new Error('Unable to read blob contents in this runtime'); +} + +async function readPng(blob: Blob): Promise<{bytes: Uint8Array; dimensions: PngDimensions | null}> { + const bytes = await blobToUint8(blob); + return {bytes, dimensions: getPngDimensions(bytes)}; +} + +async function readBinary(blob: Blob): Promise { + return blobToUint8(blob); +} + +function toBase64(bytes: Uint8Array): string { + if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64'); + let binary = ''; + const chunkSize = 0x8000; + for (let i = 0; i < bytes.length; i += chunkSize) { + const chunk = bytes.subarray(i, i + chunkSize); + binary += String.fromCharCode(...chunk); + } + if (typeof btoa !== 'undefined') return btoa(binary); + throw new Error('Base64 encoding is not supported in this runtime'); +} + +export type UploadTarget = 'token' | 'chain'; + +export type UploadErrorDetail = { + field: string; + message: string; + index?: number; + code?: string; +}; + +export class UploadValidationError extends Error { + readonly status: number; + readonly details: UploadErrorDetail[]; + readonly code?: string; + + constructor(message: string, options?: {status?: number; details?: UploadErrorDetail[]; code?: string}) { + super(message); + this.name = 'UploadValidationError'; + this.status = options?.status ?? 400; + this.details = options?.details ?? []; + this.code = options?.code; + } +} + +export type TokenAsset = { + index: number; + chainId: string; + address: string; + svgBase64: string; + png32Base64: string; + png128Base64: string; +}; + +export type ChainAsset = { + chainId: string; + svgBase64: string; + png32Base64: string; + png128Base64: string; +}; + +type PrOverrides = {title?: string; body?: string}; + +export type UploadParseResult = + | {target: 'token'; tokens: TokenAsset[]; overrides: PrOverrides} + | {target: 'chain'; chain: ChainAsset; overrides: PrOverrides}; + +function normalizeString(value: unknown): string { + return typeof value === 'string' ? value.trim() : ''; +} + +function getLatestFile(form: FormData, key: string): Blob | undefined { + const entries = form.getAll(key); + for (let i = entries.length - 1; i >= 0; i--) { + const candidate = entries[i]; + if ((candidate && typeof (candidate as any).arrayBuffer === 'function') || candidate instanceof Blob) { + return candidate as unknown as Blob; + } + } + return undefined; +} + +function collectTokenIndexes(form: FormData): number[] { + const indexes = new Set(); + for (const key of form.keys()) { + const match = key.match(/^(?:chainId|svg|png32|png128)_(\d+)$/); + if (match) indexes.add(Number.parseInt(match[1], 10)); + } + return Array.from(indexes).sort((a, b) => a - b); +} + +type PendingToken = { + index: number; + chainId: string; + address: string; + svg: Blob; + png32: Blob; + png128: Blob; +}; + +async function buildTokenAssets(form: FormData): Promise { + const globalChainId = normalizeString(form.get('chainId')); + const addressesQueue = (form.getAll('address') as Array) + .map(value => normalizeString(value as string)) + .filter(Boolean); + const tokenIndexes = collectTokenIndexes(form); + + const errors: UploadErrorDetail[] = []; + const pendings: PendingToken[] = []; + let addressCursor = 0; + + for (const index of tokenIndexes) { + let hasError = false; + const rawChainId = normalizeString(form.get(`chainId_${index}`)) || globalChainId; + const address = addressesQueue[addressCursor] || ''; + if (!rawChainId) { + errors.push({index, field: `chainId_${index}`, message: 'chainId is required'}); + hasError = true; + } + if (!address) { + errors.push({index, field: 'address', message: 'address is required'}); + hasError = true; + } else if (!isEvmAddress(address)) { + errors.push({index, field: 'address', message: 'address must be a valid EVM address'}); + hasError = true; + } + + const svg = getLatestFile(form, `svg_${index}`); + if (!svg) { + errors.push({index, field: `svg_${index}`, message: 'svg file is required'}); + hasError = true; + } else if (!svg.type.includes('svg')) { + errors.push({index, field: `svg_${index}`, message: 'svg file must be image/svg+xml'}); + hasError = true; + } + + const png32 = getLatestFile(form, `png32_${index}`); + if (!png32) { + errors.push({index, field: `png32_${index}`, message: 'png32 file is required'}); + hasError = true; + } else if (!png32.type.includes('png')) { + errors.push({index, field: `png32_${index}`, message: 'png32 file must be image/png'}); + hasError = true; + } + + const png128 = getLatestFile(form, `png128_${index}`); + if (!png128) { + errors.push({index, field: `png128_${index}`, message: 'png128 file is required'}); + hasError = true; + } else if (!png128.type.includes('png')) { + errors.push({index, field: `png128_${index}`, message: 'png128 file must be image/png'}); + hasError = true; + } + + if (!hasError) { + pendings.push({ + index, + chainId: rawChainId, + address: address.toLowerCase(), + svg: svg!, + png32: png32!, + png128: png128! + }); + } + + if (address) addressCursor += 1; + } + + if (pendings.length === 0) { + if (errors.length) { + throw new UploadValidationError('Invalid token submission', {details: errors}); + } + throw new UploadValidationError('At least one token submission required', { + status: 400, + code: 'TOKEN_SUBMISSION_MISSING' + }); + } + + if (errors.length) { + throw new UploadValidationError('Invalid token submission', {details: errors}); + } + + const tokens: TokenAsset[] = []; + for (const pending of pendings) { + try { + const svgBytes = await readBinary(pending.svg); + const png32Info = await readPng(pending.png32); + assertDimensions(`token[${pending.index}].png32`, png32Info.dimensions, {width: 32, height: 32}); + const png128Info = await readPng(pending.png128); + assertDimensions(`token[${pending.index}].png128`, png128Info.dimensions, {width: 128, height: 128}); + + tokens.push({ + index: pending.index, + chainId: pending.chainId, + address: pending.address, + svgBase64: toBase64(svgBytes), + png32Base64: toBase64(png32Info.bytes), + png128Base64: toBase64(png128Info.bytes) + }); + } catch (err: any) { + throw new UploadValidationError(err?.message || 'Failed to process token assets', { + details: [ + { + index: pending.index, + field: 'files', + message: err?.message || 'Failed to process token assets' + } + ] + }); + } + } + + return tokens; +} + +async function buildChainAsset(form: FormData): Promise { + const chainId = normalizeString(form.get('chainId')); + if (!chainId) { + throw new UploadValidationError('chainId is required for chain uploads', { + details: [{field: 'chainId', message: 'chainId is required'}], + code: 'CHAIN_ID_MISSING' + }); + } + + const svg = getLatestFile(form, 'svg'); + const png32 = getLatestFile(form, 'png32'); + const png128 = getLatestFile(form, 'png128'); + + const details: UploadErrorDetail[] = []; + if (!svg) details.push({field: 'svg', message: 'svg file is required'}); + else if (!svg.type.includes('svg')) details.push({field: 'svg', message: 'svg file must be image/svg+xml'}); + if (!png32) details.push({field: 'png32', message: 'png32 file is required'}); + else if (!png32.type.includes('png')) details.push({field: 'png32', message: 'png32 file must be image/png'}); + if (!png128) details.push({field: 'png128', message: 'png128 file is required'}); + else if (!png128.type.includes('png')) details.push({field: 'png128', message: 'png128 file must be image/png'}); + + if (details.length) { + throw new UploadValidationError('Invalid chain submission', {details}); + } + + try { + const svgBytes = await readBinary(svg!); + const png32Info = await readPng(png32!); + assertDimensions('chain.png32', png32Info.dimensions, {width: 32, height: 32}); + const png128Info = await readPng(png128!); + assertDimensions('chain.png128', png128Info.dimensions, {width: 128, height: 128}); + + return { + chainId, + svgBase64: toBase64(svgBytes), + png32Base64: toBase64(png32Info.bytes), + png128Base64: toBase64(png128Info.bytes) + }; + } catch (err: any) { + throw new UploadValidationError(err?.message || 'Failed to process chain assets', { + details: [{field: 'files', message: err?.message || 'Failed to process chain assets'}] + }); + } +} + +function extractOverrides(form: FormData): PrOverrides { + const title = normalizeString(form.get('prTitle')); + const body = normalizeString(form.get('prBody')); + return { + title: title || undefined, + body: body || undefined + }; +} + +export async function parseUploadForm(form: FormData): Promise { + const targetRaw = normalizeString(form.get('target')); + const target: UploadTarget = targetRaw === 'chain' ? 'chain' : 'token'; + const overrides = extractOverrides(form); + + if (target === 'token') { + const tokens = await buildTokenAssets(form); + return {target: 'token', tokens, overrides}; + } + const chain = await buildChainAsset(form); + return {target: 'chain', chain, overrides}; +} + +export function toRepoPath(...segments: string[]): string { + return segments.map(segment => segment.replace(/^\/+|\/+$/g, '')).join('/'); +} + +export function buildPrFiles(result: UploadParseResult): Array<{path: string; contentBase64: string}> { + if (result.target === 'token') { + return result.tokens.flatMap(token => [ + {path: toRepoPath('tokens', token.chainId, token.address, 'logo.svg'), contentBase64: token.svgBase64}, + {path: toRepoPath('tokens', token.chainId, token.address, 'logo-32.png'), contentBase64: token.png32Base64}, + { + path: toRepoPath('tokens', token.chainId, token.address, 'logo-128.png'), + contentBase64: token.png128Base64 + } + ]); + } + return [ + {path: toRepoPath('chains', result.chain.chainId, 'logo.svg'), contentBase64: result.chain.svgBase64}, + {path: toRepoPath('chains', result.chain.chainId, 'logo-32.png'), contentBase64: result.chain.png32Base64}, + {path: toRepoPath('chains', result.chain.chainId, 'logo-128.png'), contentBase64: result.chain.png128Base64} + ]; +} + +export function buildDefaultPrMetadata( + result: UploadParseResult, + overrides: PrOverrides +): {title: string; body: string} { + if (result.target === 'token') { + const tokens = [...result.tokens].sort((a, b) => a.index - b.index); + const addresses = tokens.map(t => t.address); + const chains = Array.from(new Set(tokens.map(t => t.chainId))); + const defaultTitle = `feat: add token assets (${tokens.length})`; + const locations = tokens.flatMap(t => [ + `/tokens/${t.chainId}/${t.address}/logo.svg`, + `/tokens/${t.chainId}/${t.address}/logo-32.png`, + `/tokens/${t.chainId}/${t.address}/logo-128.png` + ]); + const defaultBody = [ + `Chains: ${chains.join(', ') || 'n/a'}`, + `Addresses: ${addresses.join(', ') || 'n/a'}`, + '', + 'Uploaded locations:', + ...locations.map(loc => `- ${loc}`) + ].join('\n'); + return { + title: overrides.title || defaultTitle, + body: overrides.body || defaultBody + }; + } + const chainId = result.chain.chainId; + const defaultTitle = `feat: add chain assets on ${chainId}`; + const locations = [ + `/chains/${chainId}/logo.svg`, + `/chains/${chainId}/logo-32.png`, + `/chains/${chainId}/logo-128.png` + ]; + const defaultBody = [`Chain: ${chainId}`, '', 'Uploaded locations:', ...locations.map(loc => `- ${loc}`)].join( + '\n' + ); + return { + title: overrides.title || defaultTitle, + body: overrides.body || defaultBody + }; +} diff --git a/app/api/auth/github/authorize.ts b/app/api/auth/github/authorize.ts new file mode 100644 index 0000000000..464317205c --- /dev/null +++ b/app/api/auth/github/authorize.ts @@ -0,0 +1,75 @@ +function readEnv(key: string): string | undefined { + if (typeof process !== 'undefined' && process.env) { + const value = process.env[key]; + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; + } + } + return undefined; +} + +function normalizeBaseUrl(raw: string): string { + return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`; +} + +function resolveAppBase(req: Request): string { + const appBaseExplicit = readEnv('APP_BASE_URL'); + if (appBaseExplicit && appBaseExplicit !== '/') return appBaseExplicit; + + const urlEnv = readEnv('URL'); + if (urlEnv && urlEnv !== '/') return urlEnv; + + const vercelUrl = readEnv('VERCEL_URL'); + if (vercelUrl) return normalizeBaseUrl(vercelUrl); + + const current = new URL(req.url); + const protocol = current.protocol || 'https:'; + return `${protocol}//${current.host}`; +} + +function logAuthorize(event: string, details?: Record) { + const payload = details ? JSON.stringify(details) : ''; + console.info(`[oauth-authorize] ${new Date().toISOString()} ${event}${payload ? ' ' + payload : ''}`); +} + +export const config = {runtime: 'edge'}; + +export default async function authorize(req: Request): Promise { + const githubClientId = readEnv('GITHUB_CLIENT_ID') || readEnv('VITE_GITHUB_CLIENT_ID'); + if (!githubClientId) { + logAuthorize('missing-client-id'); + return new Response('Missing GitHub client id', {status: 500}); + } + + const requestUrl = new URL(req.url); + const providedState = requestUrl.searchParams.get('state') || ''; + const clientIdFromQuery = requestUrl.searchParams.get('client_id'); + const state = providedState?.trim().length ? providedState : crypto.randomUUID(); + + if (clientIdFromQuery && clientIdFromQuery !== githubClientId) { + logAuthorize('client-id-mismatch', { + githubClientIdPrefix: githubClientId.slice(0, 6), + clientIdFromQueryPrefix: clientIdFromQuery.slice(0, 6) + }); + return new Response('GitHub client id mismatch', {status: 400}); + } + + const base = resolveAppBase(req); + const redirectUri = new URL('/api/auth/github/callback', base).toString(); + + logAuthorize('redirect', { + stateLength: state.length, + clientIdPrefix: githubClientId.slice(0, 6), + base, + redirectUri + }); + + const authorizeUrl = new URL('https://github.com/login/oauth/authorize'); + authorizeUrl.searchParams.set('client_id', githubClientId); + authorizeUrl.searchParams.set('redirect_uri', redirectUri); + authorizeUrl.searchParams.set('scope', 'public_repo'); + authorizeUrl.searchParams.set('state', state); + + return Response.redirect(authorizeUrl.toString(), 302); +} diff --git a/app/api/auth/github/callback.ts b/app/api/auth/github/callback.ts new file mode 100644 index 0000000000..235ef75e5d --- /dev/null +++ b/app/api/auth/github/callback.ts @@ -0,0 +1,323 @@ +// Inlined environment variable reading +function readEnv(key: string): string | undefined { + if (typeof process !== 'undefined' && process.env) { + const raw = process.env[key]; + if (typeof raw === 'string') { + const trimmed = raw.trim(); + return trimmed ? trimmed : undefined; + } + } + return undefined; +} + +const oauthDebugFlag = (readEnv('GITHUB_OAUTH_DEBUG') || '').toLowerCase(); +const OAUTH_DEBUG = oauthDebugFlag ? ['1', 'true', 'on', 'yes'].includes(oauthDebugFlag) : true; +const runtimeEnv = (readEnv('NODE_ENV') || readEnv('VERCEL_ENV') || 'development').toLowerCase(); +const IS_PRODUCTION = runtimeEnv === 'production'; + +function logOAuth(event: string, details?: Record) { + if (!OAUTH_DEBUG) return; + const payload = details ? JSON.stringify(details) : ''; + console.info(`[oauth-callback] ${new Date().toISOString()} ${event}${payload ? ' ' + payload : ''}`); +} + +const seenCodes = new Map(); // code -> expiresAt +const CODE_SEEN_TTL_MS = 2 * 60_000; + +function codeAlreadySeen(code: string): boolean { + const now = Date.now(); + for (const [storedCode, expiresAt] of seenCodes) { + if (expiresAt <= now) { + seenCodes.delete(storedCode); + } + } + const expiresAt = seenCodes.get(code); + if (expiresAt && expiresAt > now) { + return true; + } + seenCodes.set(code, now + CODE_SEEN_TTL_MS); + return false; +} + +let clientIdPrefixesLogged = false; +function logClientIdPrefixesOnce() { + if (clientIdPrefixesLogged || !OAUTH_DEBUG) return; + const githubClientId = readEnv('GITHUB_CLIENT_ID'); + const viteClientId = readEnv('VITE_GITHUB_CLIENT_ID'); + if (!githubClientId && !viteClientId) return; + clientIdPrefixesLogged = true; + logOAuth('env-client-id-prefixes', { + githubClientIdPrefix: githubClientId ? githubClientId.slice(0, 6) : null, + viteGithubClientIdPrefix: viteClientId ? viteClientId.slice(0, 6) : null, + prefixesMatch: githubClientId && viteClientId ? githubClientId === viteClientId : null + }); +} + +function redactSecrets(raw?: string | null): string | undefined { + if (!raw) return undefined; + return raw + .replace(/access_token=[^&\s"']+/gi, 'access_token=[REDACTED]') + .replace(/"access_token"\s*:\s*"[^"]*"/gi, '"access_token":"[REDACTED]"'); +} + +function normalizeBaseUrl(raw: string): string { + if (!raw) return raw; + return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`; +} + +function getHeader(req: any, key: string): string | undefined { + const headers = (req as any)?.headers; + if (!headers) return undefined; + const lower = key.toLowerCase(); + if (typeof headers.get === 'function') { + const value = headers.get(key) ?? headers.get(lower); + return value ?? undefined; + } + const value = headers[key] ?? headers[lower]; + if (Array.isArray(value)) return value[0]; + return typeof value === 'string' ? value : undefined; +} + +function getRequestUrl(req: any): URL { + const raw = (req as any)?.url; + if (typeof raw === 'string') { + try { + return new URL(raw); + } catch (_) { + // fall through to header-derived reconstruction + } + } + const host = getHeader(req, 'x-forwarded-host') || getHeader(req, 'host') || 'localhost:5173'; + const proto = getHeader(req, 'x-forwarded-proto') || (host.includes('localhost') ? 'http' : 'https'); + const path = typeof raw === 'string' && raw.startsWith('/') ? raw : '/api/auth/github/callback'; + return new URL(`${proto}://${host}${path}`); +} + +function resolveAppBase(req?: any): {url: string; source: string} { + if (req) { + const parsed = getRequestUrl(req); + if (parsed.origin && parsed.origin !== 'null') return {url: parsed.origin, source: 'request-url'}; + const host = getHeader(req, 'x-forwarded-host') || getHeader(req, 'host'); + const proto = getHeader(req, 'x-forwarded-proto') || (host?.includes('localhost') ? 'http' : 'https'); + if (host) return {url: `${proto}://${host}`, source: 'request-headers'}; + } + + const appBaseExplicit = readEnv('APP_BASE_URL'); + if (appBaseExplicit && appBaseExplicit !== '/') return {url: appBaseExplicit, source: 'APP_BASE_URL'}; + + const urlEnv = readEnv('URL'); + if (urlEnv && urlEnv !== '/') return {url: urlEnv, source: 'URL'}; + + const vercelUrl = readEnv('VERCEL_URL'); + if (vercelUrl) return {url: normalizeBaseUrl(vercelUrl), source: 'VERCEL_URL'}; + + return {url: 'http://localhost:5173', source: 'fallback-default'}; +} + +function buildErrorResponse(status: number, message: string, debugData?: Record): Response { + const body: Record = {error: message}; + if (!IS_PRODUCTION && debugData && Object.keys(debugData).length) { + body.debug = debugData; + } + return new Response(JSON.stringify(body), { + status, + headers: {'Content-Type': 'application/json'} + }); +} + +export const config = {runtime: 'edge'}; + +export default async function (req: any): Promise { + const startedAt = Date.now(); + const requestId = getHeader(req, 'x-request-id') || getHeader(req, 'x-vercel-id') || undefined; + const parsedUrl = getRequestUrl(req); + const code = parsedUrl.searchParams.get('code'); + const state = parsedUrl.searchParams.get('state') || ''; + const logContextBase = { + reqId: requestId, + queryKeys: Array.from(parsedUrl.searchParams.keys()), + hasCode: Boolean(code), + stateLength: state.length + }; + logOAuth('start', logContextBase); + logClientIdPrefixesOnce(); + try { + if (!code) { + logOAuth('missing-code', {...logContextBase, durationMs: Date.now() - startedAt}); + return buildErrorResponse(400, 'Missing code'); + } + + const codePrefix = code.slice(0, 8); + if (codeAlreadySeen(code)) { + logOAuth('duplicate-code', { + ...logContextBase, + durationMs: Date.now() - startedAt, + codePrefix + }); + return buildErrorResponse(400, 'OAuth code already used', {codePrefix}); + } + + const githubClientId = readEnv('GITHUB_CLIENT_ID'); + const viteClientId = readEnv('VITE_GITHUB_CLIENT_ID'); + const clientId = githubClientId || viteClientId; + const clientIdSource = githubClientId ? 'GITHUB_CLIENT_ID' : viteClientId ? 'VITE_GITHUB_CLIENT_ID' : 'missing'; + const clientSecret = readEnv('GITHUB_CLIENT_SECRET'); + logOAuth('env-evaluated', { + ...logContextBase, + hasClientId: Boolean(clientId), + hasClientSecret: Boolean(clientSecret), + clientIdSource, + githubClientIdPrefix: githubClientId ? githubClientId.slice(0, 6) : null, + viteGithubClientIdPrefix: viteClientId ? viteClientId.slice(0, 6) : null, + clientIdsMatch: githubClientId && viteClientId ? githubClientId === viteClientId : null + }); + if (!clientId || !clientSecret) { + logOAuth('missing-env', {...logContextBase, durationMs: Date.now() - startedAt}); + return buildErrorResponse(500, 'Missing GitHub OAuth env vars', { + hasClientId: !!clientId, + hasClientSecret: !!clientSecret + }); + } + + const {url: appBase} = resolveAppBase(req); + const redirectUri = new URL('/api/auth/github/callback', appBase).toString(); + + const timeoutRaw = readEnv('GITHUB_OAUTH_TIMEOUT_MS'); + const timeoutParsed = timeoutRaw ? Number(timeoutRaw) : Number.NaN; + const tokenExchangeTimeoutMs = Number.isFinite(timeoutParsed) && timeoutParsed > 0 ? timeoutParsed : 8000; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), tokenExchangeTimeoutMs); + logOAuth('exchange-start', { + ...logContextBase, + timeoutMs: tokenExchangeTimeoutMs, + redirectUri, + codePrefix, + clientIdSource + }); + + let tokenRes: Response; + try { + const body = new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + code, + redirect_uri: redirectUri + }); + tokenRes = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: body.toString(), + signal: controller.signal + }); + } catch (fetchError: any) { + clearTimeout(timeoutId); + const isAbort = fetchError?.name === 'AbortError'; + logOAuth('exchange-error', { + ...logContextBase, + durationMs: Date.now() - startedAt, + error: isAbort ? 'timeout' : fetchError?.message || 'fetch failed', + codePrefix, + clientIdSource + }); + return buildErrorResponse( + isAbort ? 504 : 502, + isAbort ? 'GitHub token exchange timed out' : 'GitHub token exchange failed' + ); + } + clearTimeout(timeoutId); + logOAuth('exchange-response', { + ...logContextBase, + durationMs: Date.now() - startedAt, + status: tokenRes.status, + codePrefix, + clientIdSource + }); + if (!tokenRes.ok) { + const text = await tokenRes.text(); + const bodyPreview = redactSecrets(text)?.slice(0, 200); + logOAuth('exchange-non-ok', { + ...logContextBase, + status: tokenRes.status, + bodyPreview, + codePrefix, + clientIdSource + }); + return buildErrorResponse(502, 'GitHub token request failed', { + status: tokenRes.status, + bodyPreview + }); + } + + const tokenResClone = tokenRes.clone(); + let tokenJson: {access_token?: string; error?: string} | undefined; + try { + tokenJson = (await tokenRes.json()) as {access_token?: string; error?: string}; + } catch (parseErr: any) { + const fallbackText = await tokenResClone.text(); + const bodyPreview = redactSecrets(fallbackText)?.slice(0, 200); + logOAuth('exchange-parse-error', { + ...logContextBase, + durationMs: Date.now() - startedAt, + error: parseErr?.message || 'unable to parse json', + bodyPreview, + codePrefix, + clientIdSource + }); + return buildErrorResponse(502, 'Invalid response from GitHub token exchange', { + error: parseErr?.message || 'unable to parse json', + bodyPreview + }); + } + + const accessToken = tokenJson?.access_token; + if (!accessToken) { + logOAuth('missing-access-token', { + ...logContextBase, + durationMs: Date.now() - startedAt, + githubError: tokenJson?.error || null, + codePrefix, + clientIdSource, + redirectUri + }); + return buildErrorResponse(502, 'No access_token in response', { + hasErrorField: typeof tokenJson?.error === 'string' + }); + } + + const {url, source: appBaseSource} = resolveAppBase(req); + const redirect = new URL('/auth/github/success', url); + redirect.searchParams.set('token', accessToken); + redirect.searchParams.set('state', state); + logOAuth('redirect', { + ...logContextBase, + durationMs: Date.now() - startedAt, + appBase: url, + appBaseSource, + codePrefix, + clientIdSource + }); + + const location = redirect.toString(); + return new Response(null, { + status: 302, + headers: { + Location: location, + 'Cache-Control': 'no-store' + } + }); + } catch (e: any) { + logOAuth('unhandled-error', { + ...logContextBase, + durationMs: Date.now() - startedAt, + error: e?.message || 'OAuth callback failed' + }); + return buildErrorResponse(500, 'OAuth callback failed', { + error: e?.message || 'OAuth callback failed', + name: e?.name + }); + } +} diff --git a/app/api/auth/github/me.ts b/app/api/auth/github/me.ts new file mode 100644 index 0000000000..984c1f6575 --- /dev/null +++ b/app/api/auth/github/me.ts @@ -0,0 +1,56 @@ +export const config = {runtime: 'edge'}; + +function jsonResponse(body: unknown, init: ResponseInit = {}) { + const headers = new Headers(init.headers); + headers.set('Content-Type', 'application/json'); + headers.set('Cache-Control', 'no-store'); + return new Response(JSON.stringify(body), {...init, headers}); +} + +export default async function handler(req: Request): Promise { + if (req.method !== 'GET') { + return jsonResponse({error: 'Method not allowed'}, {status: 405, headers: {Allow: 'GET'}}); + } + + const authHeader = req.headers.get('authorization') || req.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return jsonResponse({error: 'Missing GitHub token'}, {status: 401}); + } + + const token = authHeader.slice('Bearer '.length).trim(); + if (!token) { + return jsonResponse({error: 'Missing GitHub token'}, {status: 401}); + } + + try { + const res = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json' + } + }); + + if (res.status === 401 || res.status === 403) { + return jsonResponse({error: 'Unauthorized'}, {status: res.status}); + } + + if (!res.ok) { + const text = await res.text(); + return jsonResponse({error: text || 'Failed to fetch profile'}, {status: 502}); + } + + const data = (await res.json()) as {login: string; name?: string; avatar_url?: string; html_url?: string}; + return jsonResponse( + { + login: data.login, + name: data.name ?? null, + avatarUrl: data.avatar_url ?? null, + htmlUrl: data.html_url ?? null + }, + {status: 200} + ); + } catch (error: any) { + const message = typeof error?.message === 'string' ? error.message : 'Failed to contact GitHub'; + return jsonResponse({error: message}, {status: 500}); + } +} diff --git a/app/api/erc20-name.ts b/app/api/erc20-name.ts new file mode 100644 index 0000000000..cc1f6ff9da --- /dev/null +++ b/app/api/erc20-name.ts @@ -0,0 +1,291 @@ +// Inlined environment variable reading +function readEnv(key: string): string | undefined { + if (typeof process !== 'undefined' && process.env) { + const raw = process.env[key]; + if (typeof raw === 'string') { + const trimmed = raw.trim(); + return trimmed ? trimmed : undefined; + } + } + return undefined; +} + +// Inlined EVM utilities +const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/i; + +const DEFAULT_RPC_URLS: Readonly>> = Object.freeze({ + 1: 'https://cloudflare-eth.com', + 10: 'https://mainnet.optimism.io', + 100: 'https://rpc.gnosischain.com', + 137: 'https://polygon-rpc.com', + 250: 'https://rpc.ankr.com/fantom', + 42161: 'https://arb1.arbitrum.io/rpc', + 8453: 'https://mainnet.base.org' +}); + +function isEvmAddress(address: string): boolean { + if (typeof address !== 'string') return false; + return ADDRESS_REGEX.test(address.trim()); +} + +function getRpcUrl(chainId: number): string | undefined { + if (!Number.isInteger(chainId)) return undefined; + const keys = [`VITE_RPC_URI_FOR_${chainId}`, `VITE_RPC_${chainId}`, `RPC_URI_FOR_${chainId}`, `RPC_${chainId}`]; + for (const key of keys) { + const fromEnv = readEnv(key); + if (fromEnv) return fromEnv; + } + return DEFAULT_RPC_URLS[chainId]; +} + +function normalizeHex(value: string): string { + const trimmed = (value || '').trim(); + const withoutPrefix = trimmed.startsWith('0x') || trimmed.startsWith('0X') ? trimmed.slice(2) : trimmed; + if (withoutPrefix.length % 2 === 1) return `0${withoutPrefix}`; + return withoutPrefix; +} + +function hexToBytes(hex: string): Uint8Array { + const normalized = hex.toLowerCase(); + const len = Math.floor(normalized.length / 2); + const out = new Uint8Array(len); + for (let i = 0; i < len; i++) { + const byte = normalized.slice(i * 2, i * 2 + 2); + const parsed = Number.parseInt(byte, 16); + out[i] = Number.isFinite(parsed) ? parsed : 0; + } + return out; +} + +function trimNulls(input: string): string { + return input.replace(/ +$/g, ''); +} + +function bytesToUtf8(bytes: Uint8Array): string { + if (!bytes.length) return ''; + const textDecoder = new TextDecoder(); + return trimNulls(textDecoder.decode(bytes)); +} + +function decodeAbiString(resultHex: string): string { + const hex = normalizeHex(resultHex); + if (!hex) return ''; + // Dynamic ABI string: offset (ignored) + length + data bytes + if (hex.length >= 192) { + const lenHex = hex.slice(64, 128); + const declaredLength = Number.parseInt(lenHex || '0', 16); + const maxBytes = Math.floor((hex.length - 128) / 2); + const safeLength = Math.max(0, Math.min(declaredLength, maxBytes)); + const dataStart = 128; + const dataEnd = dataStart + safeLength * 2; + const dataHex = hex.slice(dataStart, dataEnd); + return bytesToUtf8(hexToBytes(dataHex)); + } + // Fixed-size bytes32 padded payload + if (hex.length === 64) { + const trimmedHex = hex.replace(/00+$/g, ''); + return bytesToUtf8(hexToBytes(trimmedHex)); + } + return bytesToUtf8(hexToBytes(hex)); +} + +export const config = {runtime: 'edge'}; + +const JSON_HEADERS = {'Content-Type': 'application/json'} as const; +const CACHE_TTL_MS = normalizePositiveInt(readEnv('ERC20_NAME_CACHE_TTL_MS'), 5 * 60 * 1000); +const CACHE_MAX_ENTRIES = normalizePositiveInt(readEnv('ERC20_NAME_CACHE_SIZE'), 256); +const RPC_TIMEOUT_MS = normalizePositiveInt(readEnv('ERC20_NAME_RPC_TIMEOUT_MS'), 10_000); + +interface CacheEntry { + value: string; + expiresAt: number; +} + +const cache = new Map(); + +export function __clearCacheForTesting(): void { + cache.clear(); +} + +type ErrorBody = {error: {code: string; message: string; details?: string}}; +type SuccessBody = {name: string; cache: {hit: boolean; expiresAt: number}}; + +type RequestBody = {chainId?: number | string; address?: string}; + +type RpcPayload = { + jsonrpc: '2.0'; + id: number; + method: 'eth_call'; + params: [{to: string; data: string}, 'latest']; +}; + +function normalizePositiveInt(input: string | undefined, fallback: number): number { + const parsed = Number.parseInt(String(input ?? '').trim(), 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function jsonResponse(status: number, body: ErrorBody | SuccessBody): Response { + return new Response(JSON.stringify(body), {status, headers: JSON_HEADERS}); +} + +function errorResponse(status: number, code: string, message: string, details?: string): Response { + return jsonResponse(status, {error: {code, message, details}}); +} + +function makeCacheKey(chainId: number, address: string): string { + return `${chainId}:${address.toLowerCase()}`; +} + +function getCachedName(chainId: number, address: string, now: number) { + const key = makeCacheKey(chainId, address); + const entry = cache.get(key); + if (!entry) return undefined; + if (entry.expiresAt <= now) { + cache.delete(key); + return undefined; + } + return {name: entry.value, expiresAt: entry.expiresAt}; +} + +function pruneExpired(now: number) { + for (const [key, entry] of cache.entries()) { + if (entry.expiresAt <= now) cache.delete(key); + } +} + +function setCachedName(chainId: number, address: string, name: string, now: number): number { + pruneExpired(now); + const key = makeCacheKey(chainId, address); + const expiresAt = now + CACHE_TTL_MS; + cache.set(key, {value: name, expiresAt}); + if (cache.size > CACHE_MAX_ENTRIES) { + const iterator = cache.keys(); + while (cache.size > CACHE_MAX_ENTRIES) { + const next = iterator.next(); + if (next.done) break; + cache.delete(next.value); + } + } + return expiresAt; +} + +function normalizeAddress(address: string): string { + return address.trim().toLowerCase(); +} + +function ensureValidRpcUrl(chainId: number, rpcCandidate: string | undefined): string | Response { + if (!rpcCandidate) { + return errorResponse( + 500, + 'RPC_NOT_CONFIGURED', + `No RPC configured for chain ${chainId}. Set VITE_RPC_URI_FOR_${chainId} or VITE_RPC_${chainId}.` + ); + } + try { + const url = new URL(rpcCandidate); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return errorResponse(500, 'RPC_INVALID_PROTOCOL', `RPC URL for chain ${chainId} must use http or https`); + } + return url.toString(); + } catch (err: any) { + return errorResponse(500, 'RPC_INVALID_URL', `RPC URL for chain ${chainId} is invalid`, err?.message); + } +} + +function buildRpcPayload(address: string): RpcPayload { + return { + jsonrpc: '2.0', + id: Math.floor(Math.random() * 1e9), + method: 'eth_call', + params: [{to: address, data: '0x06fdde03'}, 'latest'] + }; +} + +export default async function (req: Request): Promise { + if (req.method !== 'POST') return errorResponse(405, 'METHOD_NOT_ALLOWED', 'Method Not Allowed'); + let body: RequestBody; + try { + body = (await req.json()) as RequestBody; + } catch (err: any) { + return errorResponse(400, 'INVALID_JSON', 'Request body must be valid JSON', err?.message); + } + const chainId = Number(body?.chainId); + if (!Number.isInteger(chainId) || chainId <= 0) { + return errorResponse(400, 'INVALID_CHAIN_ID', 'chainId must be a positive integer'); + } + const rawAddress = String(body?.address ?? '').trim(); + if (!isEvmAddress(rawAddress)) { + return errorResponse(400, 'INVALID_ADDRESS', 'Address must be a valid EVM address'); + } + const canonicalAddress = normalizeAddress(rawAddress); + const now = Date.now(); + const cached = getCachedName(chainId, canonicalAddress, now); + if (cached) { + return jsonResponse(200, {name: cached.name, cache: {hit: true, expiresAt: cached.expiresAt}}); + } + const rpcCandidate = getRpcUrl(chainId); + const rpc = ensureValidRpcUrl(chainId, rpcCandidate); + if (rpc instanceof Response) return rpc; + const payload = buildRpcPayload(canonicalAddress); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), RPC_TIMEOUT_MS); + let rpcResponse: Response; + try { + rpcResponse = await fetch(rpc, { + method: 'POST', + headers: {Accept: 'application/json', 'Content-Type': 'application/json'}, + body: JSON.stringify(payload), + signal: controller.signal + }); + } catch (err: any) { + clearTimeout(timeout); + const isAbort = err?.name === 'AbortError'; + return errorResponse( + 502, + 'RPC_REQUEST_FAILED', + isAbort ? 'RPC request timed out' : 'RPC request failed', + isAbort ? undefined : err?.message + ); + } + clearTimeout(timeout); + if (!rpcResponse.ok) { + const bodyText = await rpcResponse.text().catch(() => ''); + return errorResponse( + 502, + 'RPC_HTTP_ERROR', + `RPC responded with HTTP ${rpcResponse.status}`, + bodyText?.slice?.(0, 300) + ); + } + let rpcJson: any; + try { + rpcJson = await rpcResponse.json(); + } catch (err: any) { + const fallback = await rpcResponse.text().catch(() => ''); + return errorResponse( + 502, + 'RPC_PARSE_ERROR', + 'RPC response was not valid JSON', + fallback?.slice?.(0, 300) || err?.message + ); + } + if (rpcJson?.error) { + const message = rpcJson.error?.message || 'RPC error'; + return errorResponse(502, 'RPC_JSON_ERROR', message); + } + const result: string | undefined = rpcJson?.result; + if (!result || result === '0x') { + return errorResponse(404, 'EMPTY_RESULT', 'Contract returned empty result'); + } + let decoded: string; + try { + decoded = decodeAbiString(result); + } catch (err: any) { + return errorResponse(500, 'DECODE_ERROR', 'Failed to decode contract response', err?.message); + } + if (!decoded.trim()) { + return errorResponse(404, 'EMPTY_RESULT', 'Contract returned empty name'); + } + const expiresAt = setCachedName(chainId, canonicalAddress, decoded, now); + return jsonResponse(200, {name: decoded, cache: {hit: false, expiresAt}}); +} diff --git a/app/api/github.ts b/app/api/github.ts new file mode 100644 index 0000000000..0bdc3f1a40 --- /dev/null +++ b/app/api/github.ts @@ -0,0 +1,389 @@ +type RepoInfo = { + default_branch: string; +}; + +type RefObject = { + object: {sha: string}; +}; + +async function gh(token: string, method: string, url: string, body?: unknown): Promise { + const res = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : undefined + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`${method} ${url} -> ${res.status}: ${text}`); + } + return (await res.json()) as T; +} + +export async function getUserLogin(token: string): Promise { + const data = await gh<{login: string}>(token, 'GET', 'https://api.github.com/user'); + return data.login; +} + +export async function getRepoInfo(token: string, owner: string, repo: string): Promise { + return gh(token, 'GET', `https://api.github.com/repos/${owner}/${repo}`); +} + +export async function getHeadRef(token: string, owner: string, repo: string, branch: string): Promise { + // GitHub API path uses 'refs' (plural) + return gh(token, 'GET', `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`); +} + +export async function getCommit( + token: string, + owner: string, + repo: string, + commitSha: string +): Promise<{sha: string; tree: {sha: string}}> { + return gh(token, 'GET', `https://api.github.com/repos/${owner}/${repo}/git/commits/${commitSha}`); +} + +type BranchContext = { + branch: string; + baseCommitSha: string; + baseTreeSha: string; +}; + +async function loadBranchContext(token: string, owner: string, repo: string, branch: string): Promise { + const headRef = await getHeadRef(token, owner, repo, branch); + const baseCommitSha = headRef.object.sha; + const baseCommit = await getCommit(token, owner, repo, baseCommitSha); + return {branch, baseCommitSha, baseTreeSha: baseCommit.tree.sha}; +} + +export async function createBlob( + token: string, + owner: string, + repo: string, + contentBase64: string +): Promise<{sha: string}> { + return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/blobs`, { + content: contentBase64, + encoding: 'base64' + }); +} + +export async function createTree( + token: string, + owner: string, + repo: string, + baseTreeSha: string, + entries: Array<{path: string; mode: string; type: string; sha: string}> +): Promise<{sha: string}> { + return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/trees`, { + base_tree: baseTreeSha, + tree: entries + }); +} + +export async function createCommit( + token: string, + owner: string, + repo: string, + message: string, + treeSha: string, + parentSha: string +): Promise<{sha: string}> { + return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/commits`, { + message, + tree: treeSha, + parents: [parentSha] + }); +} + +export async function createRef( + token: string, + owner: string, + repo: string, + branch: string, + sha: string +): Promise<{ref: string}> { + return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/refs`, { + ref: `refs/heads/${branch}`, + sha + }); +} + +export async function createPullRequest( + token: string, + owner: string, + repo: string, + title: string, + head: string, + base: string, + body: string +): Promise<{html_url: string}> { + return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/pulls`, {title, head, base, body}); +} + +async function sleep(ms: number) { + return new Promise(r => setTimeout(r, ms)); +} + +async function commitExists(token: string, owner: string, repo: string, commitSha: string): Promise { + try { + await getCommit(token, owner, repo, commitSha); + return true; + } catch { + return false; + } +} + +async function syncForkWithUpstream(token: string, owner: string, repo: string, branch: string) { + try { + await gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/merge-upstream`, { + branch + }); + } catch (e: any) { + const msg = String(e?.message || ''); + if (msg.includes('409') || msg.includes('422')) { + throw new Error( + 'Unable to sync your fork with the upstream repository. Please update your fork to match upstream and retry.' + ); + } + throw e; + } +} + +async function ensureForkHasCommit(token: string, owner: string, repo: string, branch: string, commitSha: string) { + const exists = await commitExists(token, owner, repo, commitSha); + if (exists) return; + await syncForkWithUpstream(token, owner, repo, branch); + const stillMissing = !(await commitExists(token, owner, repo, commitSha)); + if (stillMissing) + throw new Error('Unable to prepare fork for PR creation. Please sync your fork with upstream and try again.'); +} + +async function commitFilesToBranch(params: { + token: string; + owner: string; + repo: string; + branchName: string; + commitMessage: string; + files: Array<{path: string; contentBase64: string}>; + baseCommitSha: string; + baseTreeSha: string; +}) { + const {token, owner, repo, branchName, commitMessage, files, baseCommitSha, baseTreeSha} = params; + const blobShas: Array<{path: string; sha: string}> = []; + for (const file of files) { + const blob = await createBlob(token, owner, repo, file.contentBase64); + blobShas.push({path: file.path, sha: blob.sha}); + } + const tree = await createTree( + token, + owner, + repo, + baseTreeSha, + blobShas.map(entry => ({path: entry.path, mode: '100644', type: 'blob', sha: entry.sha})) + ); + const commit = await createCommit(token, owner, repo, commitMessage, tree.sha, baseCommitSha); + await createRef(token, owner, repo, branchName, commit.sha); + return commit.sha; +} + +export async function ensureFork( + token: string, + baseOwner: string, + baseRepo: string, + login?: string +): Promise<{owner: string; repo: string}> { + const user = login || (await getUserLogin(token)); + // Trigger fork (idempotent) + await gh(token, 'POST', `https://api.github.com/repos/${baseOwner}/${baseRepo}/forks`); + // Poll for availability + for (let i = 0; i < 10; i++) { + try { + await gh(token, 'GET', `https://api.github.com/repos/${user}/${baseRepo}`); + return {owner: user, repo: baseRepo}; + } catch { + await sleep(800); + } + } + // Last try; let it throw if still not ready + await gh(token, 'GET', `https://api.github.com/repos/${user}/${baseRepo}`); + return {owner: user, repo: baseRepo}; +} + +async function openPrWithFilesDirect(params: { + token: string; + owner: string; // repo where commit happens + repo: string; + branchName: string; + commitMessage: string; + prTitle: string; + prBody: string; + files: Array<{path: string; contentBase64: string}>; +}): Promise { + const {token, owner, repo} = params; + const repoInfo = await getRepoInfo(token, owner, repo); + const baseBranch = repoInfo.default_branch || 'main'; + const context = await loadBranchContext(token, owner, repo, baseBranch); + await commitFilesToBranch({ + token, + owner, + repo, + branchName: params.branchName, + commitMessage: params.commitMessage, + files: params.files, + baseCommitSha: context.baseCommitSha, + baseTreeSha: context.baseTreeSha + }); + const pr = await createPullRequest( + token, + owner, + repo, + params.prTitle, + params.branchName, + context.branch, + params.prBody + ); + return pr.html_url; +} + +export async function openPrWithFilesToBaseFromHead(params: { + token: string; + baseOwner: string; // where PR is opened + baseRepo: string; + headOwner: string; // where branch/commit happen + headRepo: string; + branchName: string; + commitMessage: string; + prTitle: string; + prBody: string; + files: Array<{path: string; contentBase64: string}>; +}) { + const {token, baseOwner, baseRepo, headOwner, headRepo} = params; + + const baseInfo = await getRepoInfo(token, baseOwner, baseRepo); + const baseBranch = baseInfo.default_branch || 'main'; + const baseContext = await loadBranchContext(token, baseOwner, baseRepo, baseBranch); + + if (headOwner !== baseOwner || headRepo !== baseRepo) { + await ensureForkHasCommit(token, headOwner, headRepo, baseContext.branch, baseContext.baseCommitSha); + } + + await commitFilesToBranch({ + token, + owner: headOwner, + repo: headRepo, + branchName: params.branchName, + commitMessage: params.commitMessage, + files: params.files, + baseCommitSha: baseContext.baseCommitSha, + baseTreeSha: baseContext.baseTreeSha + }); + + // Open PR in base repo using head owner:branch + const pr = await createPullRequest( + token, + baseOwner, + baseRepo, + params.prTitle, + `${headOwner}:${params.branchName}`, + baseBranch, + params.prBody + ); + return pr.html_url; +} + +export async function openPrWithFilesForkAware(params: { + token: string; + baseOwner: string; // target repo owner (org) + baseRepo: string; + branchName: string; + commitMessage: string; + prTitle: string; + prBody: string; + files: Array<{path: string; contentBase64: string}>; +}) { + // Try direct (may fail with 403 due to org OAuth restrictions) + try { + return await openPrWithFilesDirect({ + token: params.token, + owner: params.baseOwner, + repo: params.baseRepo, + branchName: params.branchName, + commitMessage: params.commitMessage, + prTitle: params.prTitle, + prBody: params.prBody, + files: params.files + }); + } catch (e: any) { + const msg = String(e?.message || ''); + const is403 = msg.includes(' 403:') || msg.includes('status":"403'); + if (!is403) throw e; + // Fallback to fork flow + const login = await getUserLogin(params.token); + const head = await ensureFork(params.token, params.baseOwner, params.baseRepo, login); + return openPrWithFilesToBaseFromHead({ + token: params.token, + baseOwner: params.baseOwner, + baseRepo: params.baseRepo, + headOwner: head.owner, + headRepo: head.repo, + branchName: params.branchName, + commitMessage: params.commitMessage, + prTitle: params.prTitle, + prBody: params.prBody, + files: params.files + }); + } +} + +export async function openPrWithFiles(params: { + token: string; + owner: string; + repo: string; + baseBranch?: string; + branchName: string; + commitMessage: string; + prTitle: string; + prBody: string; + files: Array<{path: string; contentBase64: string}>; // repo-relative paths +}) { + const {token, owner, repo} = params; + const repoInfo = await getRepoInfo(token, owner, repo); + const baseBranch = params.baseBranch || repoInfo.default_branch || 'main'; + const headRef = await getHeadRef(token, owner, repo, baseBranch); + const baseCommitSha = headRef.object.sha; + const baseCommit = await getCommit(token, owner, repo, baseCommitSha); + + // Create tree entries from provided files (already base64-encoded) + const blobShas: Array<{path: string; sha: string}> = []; + for (const f of params.files) { + const blob = await createBlob(token, owner, repo, f.contentBase64); + blobShas.push({path: f.path, sha: blob.sha}); + } + + const tree = await createTree( + token, + owner, + repo, + baseCommit.tree.sha, + blobShas.map(b => ({path: b.path, mode: '100644', type: 'blob', sha: b.sha})) + ); + + const commit = await createCommit(token, owner, repo, params.commitMessage, tree.sha, baseCommitSha); + + await createRef(token, owner, repo, params.branchName, commit.sha); + + const pr = await createPullRequest( + token, + owner, + repo, + params.prTitle, + params.branchName, + baseBranch, + params.prBody + ); + return pr.html_url; +} diff --git a/app/api/health.ts b/app/api/health.ts new file mode 100644 index 0000000000..c70bb1eb05 --- /dev/null +++ b/app/api/health.ts @@ -0,0 +1,8 @@ +export const config = {runtime: 'edge'}; + +export default async function (): Promise { + return new Response(JSON.stringify({ok: true, service: 'image-tools-api'}), { + status: 200, + headers: {'Content-Type': 'application/json'} + }); +} diff --git a/app/api/ping.ts b/app/api/ping.ts new file mode 100644 index 0000000000..26c0ab3c36 --- /dev/null +++ b/app/api/ping.ts @@ -0,0 +1,8 @@ +export const config = {runtime: 'edge'}; + +export default async function (): Promise { + return new Response(JSON.stringify({ok: true, service: 'image-tools'}), { + status: 200, + headers: {'Content-Type': 'application/json'} + }); +} diff --git a/app/api/upload.ts b/app/api/upload.ts new file mode 100644 index 0000000000..85a1b8ed46 --- /dev/null +++ b/app/api/upload.ts @@ -0,0 +1,129 @@ +export const config = {runtime: 'edge'}; + +import {UploadValidationError, buildDefaultPrMetadata, buildPrFiles, parseUploadForm} from './_lib/upload'; +import {getUserLogin, openPrWithFilesForkAware} from './github'; + +function readEnv(key: string): string | undefined { + if (typeof process !== 'undefined' && process.env) { + const raw = process.env[key]; + if (typeof raw === 'string') { + const trimmed = raw.trim(); + return trimmed ? trimmed : undefined; + } + } + return undefined; +} + +const CANONICAL_OWNER = 'yearn'; +const CANONICAL_REPO = 'tokenAssets'; + +type TargetRepo = { + owner: string; + repo: string; + reason: 'canonical' | 'override'; + allowOverride: boolean; +}; + +function getHeader(req: any, key: string): string | undefined { + const headers = (req as any)?.headers; + if (!headers) return undefined; + const lower = key.toLowerCase(); + if (typeof headers.get === 'function') { + const value = headers.get(key) ?? headers.get(lower); + return value ?? undefined; + } + const value = headers[key] ?? headers[lower]; + if (Array.isArray(value)) return value[0]; + return typeof value === 'string' ? value : undefined; +} + +function resolveTargetRepo(): TargetRepo { + const envOwner = readEnv('REPO_OWNER'); + const envRepo = readEnv('REPO_NAME'); + const vercelOwner = readEnv('VERCEL_GIT_REPO_OWNER'); + const vercelRepo = readEnv('VERCEL_GIT_REPO_SLUG'); + const allowOverrideRaw = readEnv('ALLOW_REPO_OVERRIDE'); + const allowOverride = (allowOverrideRaw || '').toLowerCase() === 'true'; + + let owner = CANONICAL_OWNER; + let repo = CANONICAL_REPO; + let reason: TargetRepo['reason'] = 'canonical'; + + if (envOwner && envRepo) { + const envOwnerLower = envOwner.toLowerCase(); + const envRepoLower = envRepo.toLowerCase(); + const vercelOwnerLower = vercelOwner?.toLowerCase(); + const vercelRepoLower = vercelRepo?.toLowerCase(); + const isSelfDeploy = + Boolean(vercelOwnerLower && vercelRepoLower) && + envOwnerLower === vercelOwnerLower && + envRepoLower === vercelRepoLower; + if (allowOverride || !isSelfDeploy) { + owner = envOwner; + repo = envRepo; + reason = 'override'; + } + } + + console.info('[api/upload] target repository resolved', { + owner, + repo, + reason, + allowOverride + }); + + return {owner, repo, reason, allowOverride}; +} + +function jsonResponse(status: number, body: Record): Response { + return new Response(JSON.stringify(body), { + status, + headers: {'Content-Type': 'application/json'} + }); +} + +export default async function handler(req: Request): Promise { + if (req.method !== 'POST') return new Response('Method Not Allowed', {status: 405}); + try { + const auth = getHeader(req, 'authorization') || ''; + const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; + if (!token) { + return jsonResponse(401, {error: 'Missing GitHub token', code: 'AUTH_REQUIRED'}); + } + + const form = await req.formData(); + const parsed = await parseUploadForm(form); + const prFiles = buildPrFiles(parsed); + const metadata = buildDefaultPrMetadata(parsed, parsed.overrides); + const {owner, repo} = resolveTargetRepo(); + const login = await getUserLogin(token).catch(() => 'user'); + const branchName = `${login}-image-tools-${parsed.target}-${Date.now()}`; + + const prUrl = await openPrWithFilesForkAware({ + token, + baseOwner: owner, + baseRepo: repo, + branchName, + commitMessage: metadata.title, + prTitle: metadata.title, + prBody: metadata.body, + files: prFiles + }); + + return jsonResponse(200, { + ok: true, + prUrl, + repository: {owner, repo} + }); + } catch (error: any) { + if (error instanceof UploadValidationError) { + return jsonResponse(error.status, { + error: error.message, + details: error.details, + code: error.code || 'UPLOAD_VALIDATION_FAILED' + }); + } + console.error('[api/upload] unexpected error', error); + return jsonResponse(500, {error: 'Upload failed'}); + } +} diff --git a/app/image-tools/bun.lock b/app/bun.lock similarity index 65% rename from app/image-tools/bun.lock rename to app/bun.lock index ac2d261425..b9691ae759 100644 --- a/app/image-tools/bun.lock +++ b/app/bun.lock @@ -4,37 +4,33 @@ "": { "name": "image-tools", "dependencies": { - "@headlessui/react": "^2.1.10", - "@tanstack/react-query": "^5.51.3", - "@tanstack/react-router": "^1.47.0", - "@types/react": "^19.1.12", + "@headlessui/react": "^2.2.8", + "@tanstack/react-query": "^5.89.0", + "@tanstack/react-router": "^1.131.48", + "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "image-size": "^1.0.2", - "multer": "^1.4.5-lts.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sharp": "^0.33.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/multer": "^1.4.11", - "@vitejs/plugin-react": "^4.2.0", - "autoprefixer": "^10.4.19", - "postcss": "^8.4.41", - "tailwindcss": "^3.4.9", - "tsx": "^4.7.0", - "typescript": "^5.4.0", - "vite": "^5.2.0", + "@biomejs/biome": "^1.9.2", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "jsdom": "^24.1.3", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "vite": "^5.4.20", + "vitest": "^2.1.9", }, }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], @@ -73,7 +69,33 @@ "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], - "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], @@ -137,45 +159,7 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@headlessui/react": ["@headlessui/react@2.2.7", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@headlessui/react": ["@headlessui/react@2.2.8", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-vkiZulDC0lFeTrZTbA4tHvhZHvkUb2PFh5xJ1BvWAZdRK0fayMKO1QEO4inWkXxK1i0I1rcwwu1d6mo0K7Pcbw=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -259,17 +243,17 @@ "@tanstack/history": ["@tanstack/history@1.131.2", "", {}, "sha512-cs1WKawpXIe+vSTeiZUuSBy8JFjEuDgdMKZFRLKwQysKo8y2q6Q1HvS74Yw+m5IhOW1nTZooa6rlgdfXcgFAaw=="], - "@tanstack/query-core": ["@tanstack/query-core@5.87.1", "", {}, "sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA=="], + "@tanstack/query-core": ["@tanstack/query-core@5.89.0", "", {}, "sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q=="], - "@tanstack/react-query": ["@tanstack/react-query@5.87.1", "", { "dependencies": { "@tanstack/query-core": "5.87.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-YKauf8jfMowgAqcxj96AHs+Ux3m3bWT1oSVKamaRPXSnW2HqSznnTCEkAVqctF1e/W9R/mPcyzzINIgpOH94qg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.89.0", "", { "dependencies": { "@tanstack/query-core": "5.89.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A=="], - "@tanstack/react-router": ["@tanstack/react-router@1.131.35", "", { "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/react-store": "^0.7.0", "@tanstack/router-core": "1.131.35", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-2mwHgwoSs4wih67jfl2TjcF4enYpLpY0TljE+Sl1njZ01CWLrrQgjQ6tEuVA24Pm5re4V01A3abKvDtN1miQ9Q=="], + "@tanstack/react-router": ["@tanstack/react-router@1.131.48", "", { "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/react-store": "^0.7.0", "@tanstack/router-core": "1.131.48", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-iT9k/+J4vkoXyI1lBu0StSCLXgfOIMf/IDPh+pZ5HhMPab/wx0PDgIFFgEq9qM1CCykDnKqqeDY0QZWBUS4V1A=="], "@tanstack/react-store": ["@tanstack/react-store@0.7.4", "", { "dependencies": { "@tanstack/store": "0.7.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-DyG1e5Qz/c1cNLt/NdFbCA7K1QGuFXQYT6EfUltYMJoQ4LzBOGnOl5IjuxepNcRtmIKkGpmdMzdFZEkevgU9bQ=="], "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], - "@tanstack/router-core": ["@tanstack/router-core@1.131.35", "", { "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/store": "^0.7.0", "cookie-es": "^1.2.2", "seroval": "^1.3.2", "seroval-plugins": "^1.3.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-wS+Tcczo3+63LbrRKQGrpUSa9yws0V/fg32KK/tOi0BDlloVM3KTED3UP2hMVyqaMgts6jK7n1b/cEGWOlLdAA=="], + "@tanstack/router-core": ["@tanstack/router-core@1.131.48", "", { "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/store": "^0.7.0", "cookie-es": "^1.2.2", "seroval": "^1.3.2", "seroval-plugins": "^1.3.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-Zw024eJECTLn57bR8oncR4YUTvQ8P41pDnVEXevWPuR6wdKRsIUmhfJowzgf4ppF9Lgl5DUWEhOcj4Awr4tTOQ=="], "@tanstack/store": ["@tanstack/store@0.7.4", "", {}, "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg=="], @@ -283,41 +267,31 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], - "@types/multer": ["@types/multer@1.4.13", "", { "dependencies": { "@types/express": "*" } }, "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw=="], + "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], - "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], - "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + "@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], - "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], + "@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], - "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + "@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + "@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], @@ -327,11 +301,11 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="], - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], @@ -339,107 +313,89 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.25.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], "caniuse-lite": ["caniuse-lite@1.0.30001741", "", {}, "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="], - - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], - "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], - "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], - - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], - "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.214", "", {}, "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -447,16 +403,12 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -477,19 +429,17 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], @@ -503,7 +453,7 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], "isbot": ["isbot@5.1.30", "", {}, "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA=="], @@ -515,6 +465,8 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "jsdom": ["jsdom@24.1.3", "", { "dependencies": { "cssstyle": "^4.0.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.12", "parse5": "^7.1.2", "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.4", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^2.11.2" }, "optionalPeers": ["canvas"] }, "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -525,61 +477,47 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - - "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "multer": ["multer@1.4.5-lts.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.0.0", "concat-stream": "^1.5.2", "mkdirp": "^0.5.4", "object-assign": "^4.1.1", "type-is": "^1.6.4", "xtend": "^4.0.0" } }, "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "node-releases": ["node-releases@2.0.20", "", {}, "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + "nwsapi": ["nwsapi@2.2.22", "", {}, "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -587,7 +525,9 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -611,20 +551,14 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], @@ -633,10 +567,10 @@ "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -645,56 +579,40 @@ "rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -703,6 +621,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], @@ -715,9 +635,21 @@ "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -725,82 +657,86 @@ "tsx": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="], - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], - "vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="], + "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], - "@babel/core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], - "@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite-node/vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="], + + "vitest/vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="], + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -847,8 +783,102 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "vitest/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], } } diff --git a/app/image-tools/api/auth/github/callback.ts b/app/image-tools/api/auth/github/callback.ts deleted file mode 100644 index 36a3c20da6..0000000000 --- a/app/image-tools/api/auth/github/callback.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const config = { runtime: 'edge' }; - -export default async function (req: Request): Promise { - try { - const url = new URL(req.url); - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state') || ''; - if (!code) { - return new Response(JSON.stringify({ error: 'Missing code' }), { - status: 400, - headers: { 'Content-Type': 'application/json' } - }); - } - - const clientId = process.env.GITHUB_CLIENT_ID || process.env.VITE_GITHUB_CLIENT_ID; - const clientSecret = process.env.GITHUB_CLIENT_SECRET; - if (!clientId || !clientSecret) { - return new Response(JSON.stringify({ error: 'Missing GitHub OAuth env vars' }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } - - const tokenRes = await fetch('https://github.com/login/oauth/access_token', { - method: 'POST', - headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code }) - }); - if (!tokenRes.ok) { - const text = await tokenRes.text(); - return new Response(text, { status: 502 }); - } - const tokenJson = (await tokenRes.json()) as { access_token?: string }; - const accessToken = tokenJson.access_token; - if (!accessToken) { - return new Response(JSON.stringify({ error: 'No access_token in response' }), { - status: 502, - headers: { 'Content-Type': 'application/json' } - }); - } - - const appBase = process.env.APP_BASE_URL || new URL(req.url).origin; - const redirect = new URL('/auth/github/success', appBase); - redirect.searchParams.set('token', accessToken); - redirect.searchParams.set('state', state); - return Response.redirect(redirect.toString(), 302); - } catch (e: any) { - return new Response(JSON.stringify({ error: e?.message || 'OAuth callback failed' }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } -} - diff --git a/app/image-tools/api/erc20-name.ts b/app/image-tools/api/erc20-name.ts deleted file mode 100644 index 77a314da79..0000000000 --- a/app/image-tools/api/erc20-name.ts +++ /dev/null @@ -1,94 +0,0 @@ -export const config = { runtime: 'edge' }; - -function isEvmAddress(addr: string): boolean { - return /^0x[a-fA-F0-9]{40}$/.test(String(addr || '').trim()); -} - -const DEFAULT_RPCS: Partial> = { - 1: 'https://cloudflare-eth.com', - 10: 'https://mainnet.optimism.io', - 100: 'https://rpc.gnosischain.com', - 137: 'https://polygon-rpc.com', - 250: 'https://rpc.ankr.com/fantom', - 42161: 'https://arb1.arbitrum.io/rpc', - 8453: 'https://mainnet.base.org', -}; - -function getRpcUrlFromEnv(chainId: number): string | undefined { - const k1 = `VITE_RPC_URI_FOR_${chainId}`; - const k2 = `VITE_RPC_${chainId}`; - const val = (process.env as any)[k1] || (process.env as any)[k2]; - return (val as string | undefined) || DEFAULT_RPCS[chainId]; -} - -function decodeAbiString(resultHex: string): string { - const hex = resultHex.startsWith('0x') ? resultHex.slice(2) : resultHex; - if (hex.length >= 192) { - const lenHex = hex.slice(64, 128); - const len = parseInt(lenHex || '0', 16); - const dataHex = hex.slice(128, 128 + len * 2); - return Buffer.from(dataHex, 'hex').toString('utf8').replace(/\u0000+$/, ''); - } - if (hex.length === 64) { - const trimmed = hex.replace(/00+$/, ''); - return Buffer.from(trimmed, 'hex').toString('utf8').replace(/\u0000+$/, ''); - } - return Buffer.from(hex, 'hex').toString('utf8').replace(/\u0000+$/, ''); -} - -export default async function (req: Request): Promise { - if (req.method !== 'POST') return new Response('Method Not Allowed', { status: 405 }); - try { - const { chainId: chainIdRaw, address } = (await req.json()) as { - chainId?: number | string; - address?: string; - }; - const chainIdStr = String(chainIdRaw || '').trim(); - const addr = String(address || '').trim(); - const chainId = Number(chainIdStr); - if (!chainId || Number.isNaN(chainId)) - return new Response(JSON.stringify({ error: 'Invalid chainId' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!isEvmAddress(addr)) - return new Response(JSON.stringify({ error: 'Invalid address' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - const rpc = getRpcUrlFromEnv(chainId); - if (!rpc) - return new Response(JSON.stringify({ error: 'No RPC configured for chain' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - - const payload = { - jsonrpc: '2.0', - id: Math.floor(Math.random() * 1e9), - method: 'eth_call', - params: [{ to: addr, data: '0x06fdde03' }, 'latest'], - }; - const r = await fetch(rpc, { - method: 'POST', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, - body: JSON.stringify(payload), - }); - if (!r.ok) { - const bodyText = await r.text().catch(() => ''); - return new Response( - JSON.stringify({ error: `RPC HTTP ${r.status}`, details: bodyText?.slice?.(0, 300) }), - { status: 502, headers: { 'Content-Type': 'application/json' } } - ); - } - const j = await r.json().catch(async () => ({ raw: await r.text() })); - if (j?.error) { - return new Response(JSON.stringify({ error: j.error?.message || 'RPC error' }), { - status: 502, - headers: { 'Content-Type': 'application/json' }, - }); - } - const result: string | undefined = (j as any)?.result; - if (!result || result === '0x') - return new Response(JSON.stringify({ error: 'Empty result' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); - const name = decodeAbiString(result); - return new Response(JSON.stringify({ name }), { status: 200, headers: { 'Content-Type': 'application/json' } }); - } catch (e: any) { - return new Response(JSON.stringify({ error: e?.message || 'Lookup failed' }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); - } -} - diff --git a/app/image-tools/api/github.ts b/app/image-tools/api/github.ts deleted file mode 100644 index 6a31382a30..0000000000 --- a/app/image-tools/api/github.ts +++ /dev/null @@ -1,292 +0,0 @@ -type RepoInfo = { - default_branch: string; -}; - -type RefObject = { - object: { sha: string }; -}; - -async function gh(token: string, method: string, url: string, body?: unknown): Promise { - const res = await fetch(url, { - method, - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github+json', - 'Content-Type': 'application/json', - }, - body: body ? JSON.stringify(body) : undefined, - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(`${method} ${url} -> ${res.status}: ${text}`); - } - return (await res.json()) as T; -} - -export async function getUserLogin(token: string): Promise { - const data = await gh<{ login: string }>(token, 'GET', 'https://api.github.com/user'); - return data.login; -} - -export async function getRepoInfo(token: string, owner: string, repo: string): Promise { - return gh(token, 'GET', `https://api.github.com/repos/${owner}/${repo}`); -} - -export async function getHeadRef(token: string, owner: string, repo: string, branch: string): Promise { - // GitHub API path uses 'refs' (plural) - return gh(token, 'GET', `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`); -} - -export async function getCommit(token: string, owner: string, repo: string, commitSha: string): Promise<{ sha: string; tree: { sha: string } }>{ - return gh(token, 'GET', `https://api.github.com/repos/${owner}/${repo}/git/commits/${commitSha}`); -} - -export async function createBlob(token: string, owner: string, repo: string, contentBase64: string): Promise<{ sha: string }>{ - return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/blobs`, { content: contentBase64, encoding: 'base64' }); -} - -export async function createTree(token: string, owner: string, repo: string, baseTreeSha: string, entries: Array<{ path: string; mode: string; type: string; sha: string }>): Promise<{ sha: string }>{ - return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/trees`, { base_tree: baseTreeSha, tree: entries }); -} - -export async function createCommit(token: string, owner: string, repo: string, message: string, treeSha: string, parentSha: string): Promise<{ sha: string }>{ - return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/commits`, { message, tree: treeSha, parents: [parentSha] }); -} - -export async function createRef(token: string, owner: string, repo: string, branch: string, sha: string): Promise<{ ref: string }>{ - return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/git/refs`, { ref: `refs/heads/${branch}`, sha }); -} - -export async function createPullRequest(token: string, owner: string, repo: string, title: string, head: string, base: string, body: string): Promise<{ html_url: string }>{ - return gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/pulls`, { title, head, base, body }); -} - -async function sleep(ms: number) { - return new Promise((r) => setTimeout(r, ms)); -} - -async function commitExists(token: string, owner: string, repo: string, commitSha: string): Promise { - try { - await getCommit(token, owner, repo, commitSha); - return true; - } catch { - return false; - } -} - -async function syncForkWithUpstream(token: string, owner: string, repo: string, branch: string) { - try { - await gh(token, 'POST', `https://api.github.com/repos/${owner}/${repo}/merge-upstream`, { - branch, - }); - } catch (e: any) { - const msg = String(e?.message || ''); - if (msg.includes('409') || msg.includes('422')) { - throw new Error('Unable to sync your fork with the upstream repository. Please update your fork to match upstream and retry.'); - } - throw e; - } -} - -async function ensureForkHasCommit(token: string, owner: string, repo: string, branch: string, commitSha: string) { - const exists = await commitExists(token, owner, repo, commitSha); - if (exists) return; - await syncForkWithUpstream(token, owner, repo, branch); - const stillMissing = !(await commitExists(token, owner, repo, commitSha)); - if (stillMissing) throw new Error('Unable to prepare fork for PR creation. Please sync your fork with upstream and try again.'); -} - -export async function ensureFork(token: string, baseOwner: string, baseRepo: string, login?: string): Promise<{ owner: string; repo: string }>{ - const user = login || (await getUserLogin(token)); - // Trigger fork (idempotent) - await gh(token, 'POST', `https://api.github.com/repos/${baseOwner}/${baseRepo}/forks`); - // Poll for availability - for (let i = 0; i < 10; i++) { - try { - await gh(token, 'GET', `https://api.github.com/repos/${user}/${baseRepo}`); - return { owner: user, repo: baseRepo }; - } catch { - await sleep(800); - } - } - // Last try; let it throw if still not ready - await gh(token, 'GET', `https://api.github.com/repos/${user}/${baseRepo}`); - return { owner: user, repo: baseRepo }; -} - -async function openPrWithFilesDirect(params: { - token: string; - owner: string; // repo where commit happens - repo: string; - branchName: string; - commitMessage: string; - prTitle: string; - prBody: string; -}) { - const { token, owner, repo } = params; - const repoInfo = await getRepoInfo(token, owner, repo); - const baseBranch = repoInfo.default_branch; - const headRef = await getHeadRef(token, owner, repo, baseBranch); - const baseCommitSha = headRef.object.sha; - const baseCommit = await getCommit(token, owner, repo, baseCommitSha); - return { baseBranch, baseCommit }; -} - -export async function openPrWithFilesToBaseFromHead(params: { - token: string; - baseOwner: string; // where PR is opened - baseRepo: string; - headOwner: string; // where branch/commit happen - headRepo: string; - branchName: string; - commitMessage: string; - prTitle: string; - prBody: string; - files: Array<{ path: string; contentBase64: string }>; -}) { - const { token, baseOwner, baseRepo, headOwner, headRepo } = params; - - const baseInfo = await getRepoInfo(token, baseOwner, baseRepo); - const baseBranch = baseInfo.default_branch; - - const baseRef = await getHeadRef(token, baseOwner, baseRepo, baseBranch); - const baseCommitSha = baseRef.object.sha; - const baseCommit = await getCommit(token, baseOwner, baseRepo, baseCommitSha); - - if (headOwner !== baseOwner || headRepo !== baseRepo) { - const headInfo = await getRepoInfo(token, headOwner, headRepo); - const headBaseBranch = headInfo.default_branch; - await ensureForkHasCommit(token, headOwner, headRepo, headBaseBranch, baseCommitSha); - } - - const blobShas: Array<{ path: string; sha: string }> = []; - for (const f of params.files) { - const blob = await createBlob(token, headOwner, headRepo, f.contentBase64); - blobShas.push({ path: f.path, sha: blob.sha }); - } - - const tree = await createTree( - token, - headOwner, - headRepo, - baseCommit.tree.sha, - blobShas.map((b) => ({ path: b.path, mode: '100644', type: 'blob', sha: b.sha })) - ); - - const commit = await createCommit(token, headOwner, headRepo, params.commitMessage, tree.sha, baseCommitSha); - - await createRef(token, headOwner, headRepo, params.branchName, commit.sha); - - // Open PR in base repo using head owner:branch - const pr = await createPullRequest( - token, - baseOwner, - baseRepo, - params.prTitle, - `${headOwner}:${params.branchName}`, - baseBranch, - params.prBody - ); - return pr.html_url; -} - -export async function openPrWithFilesForkAware(params: { - token: string; - baseOwner: string; // target repo owner (org) - baseRepo: string; - branchName: string; - commitMessage: string; - prTitle: string; - prBody: string; - files: Array<{ path: string; contentBase64: string }>; -}) { - // Try direct (may fail with 403 due to org OAuth restrictions) - try { - const { baseBranch, baseCommit } = await openPrWithFilesDirect({ - token: params.token, - owner: params.baseOwner, - repo: params.baseRepo, - branchName: params.branchName, - commitMessage: params.commitMessage, - prTitle: params.prTitle, - prBody: params.prBody, - }); - - const blobShas: Array<{ path: string; sha: string }> = []; - for (const f of params.files) { - const blob = await createBlob(params.token, params.baseOwner, params.baseRepo, f.contentBase64); - blobShas.push({ path: f.path, sha: blob.sha }); - } - const tree = await createTree( - params.token, - params.baseOwner, - params.baseRepo, - baseCommit.tree.sha, - blobShas.map((b) => ({ path: b.path, mode: '100644', type: 'blob', sha: b.sha })) - ); - const commit = await createCommit(params.token, params.baseOwner, params.baseRepo, params.commitMessage, tree.sha, baseCommit.sha); - await createRef(params.token, params.baseOwner, params.baseRepo, params.branchName, commit.sha); - const pr = await createPullRequest(params.token, params.baseOwner, params.baseRepo, params.prTitle, params.branchName, baseBranch, params.prBody); - return pr.html_url; - } catch (e: any) { - const msg = String(e?.message || ''); - const is403 = msg.includes(' 403:') || msg.includes('status":"403'); - if (!is403) throw e; - // Fallback to fork flow - const login = await getUserLogin(params.token); - const head = await ensureFork(params.token, params.baseOwner, params.baseRepo, login); - return openPrWithFilesToBaseFromHead({ - token: params.token, - baseOwner: params.baseOwner, - baseRepo: params.baseRepo, - headOwner: head.owner, - headRepo: head.repo, - branchName: params.branchName, - commitMessage: params.commitMessage, - prTitle: params.prTitle, - prBody: params.prBody, - files: params.files, - }); - } -} - -export async function openPrWithFiles(params: { - token: string; - owner: string; - repo: string; - baseBranch?: string; - branchName: string; - commitMessage: string; - prTitle: string; - prBody: string; - files: Array<{ path: string; contentBase64: string }>; // repo-relative paths -}) { - const { token, owner, repo } = params; - const repoInfo = await getRepoInfo(token, owner, repo); - const baseBranch = params.baseBranch || repoInfo.default_branch; - const headRef = await getHeadRef(token, owner, repo, baseBranch); - const baseCommitSha = headRef.object.sha; - const baseCommit = await getCommit(token, owner, repo, baseCommitSha); - - // Create tree entries from provided files (already base64-encoded) - const blobShas: Array<{ path: string; sha: string }> = []; - for (const f of params.files) { - const blob = await createBlob(token, owner, repo, f.contentBase64); - blobShas.push({ path: f.path, sha: blob.sha }); - } - - const tree = await createTree( - token, - owner, - repo, - baseCommit.tree.sha, - blobShas.map((b) => ({ path: b.path, mode: '100644', type: 'blob', sha: b.sha })) - ); - - const commit = await createCommit(token, owner, repo, params.commitMessage, tree.sha, baseCommitSha); - - await createRef(token, owner, repo, params.branchName, commit.sha); - - const pr = await createPullRequest(token, owner, repo, params.prTitle, params.branchName, baseBranch, params.prBody); - return pr.html_url; -} diff --git a/app/image-tools/api/health.ts b/app/image-tools/api/health.ts deleted file mode 100644 index 4c44173bd8..0000000000 --- a/app/image-tools/api/health.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const config = { runtime: 'edge' }; - -export default async function (): Promise { - return new Response(JSON.stringify({ ok: true, service: 'image-tools-api' }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); -} - diff --git a/app/image-tools/api/ping.ts b/app/image-tools/api/ping.ts deleted file mode 100644 index 33397ec4c1..0000000000 --- a/app/image-tools/api/ping.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const config = { runtime: 'edge' }; - -export default async function (): Promise { - return new Response(JSON.stringify({ ok: true, service: 'image-tools' }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); -} - diff --git a/app/image-tools/api/upload.ts b/app/image-tools/api/upload.ts deleted file mode 100644 index 086bec2f9b..0000000000 --- a/app/image-tools/api/upload.ts +++ /dev/null @@ -1,190 +0,0 @@ -export const config = { runtime: 'edge' }; - -import { openPrWithFilesForkAware, getUserLogin } from './github'; - -const CANONICAL_OWNER = 'yearn'; -const CANONICAL_REPO = 'tokenAssets'; - -// Deploys triggered from personal forks should still open PRs against the -// canonical org repo unless an explicit override is opt-in via env flag. -function resolveTargetRepo(): { owner: string; repo: string } { - const envOwner = (process.env.REPO_OWNER as string)?.trim(); - const envRepo = (process.env.REPO_NAME as string)?.trim(); - const vercelOwner = (process.env.VERCEL_GIT_REPO_OWNER as string)?.trim(); - const vercelRepo = (process.env.VERCEL_GIT_REPO_SLUG as string)?.trim(); - const allowOverride = (process.env.ALLOW_REPO_OVERRIDE || '').toLowerCase() === 'true'; - - const owner = envOwner && (allowOverride || envOwner.toLowerCase() !== (vercelOwner || '').toLowerCase()) ? envOwner : CANONICAL_OWNER; - const repo = envRepo && (allowOverride || envRepo.toLowerCase() !== (vercelRepo || '').toLowerCase()) ? envRepo : CANONICAL_REPO; - - return { owner, repo }; -} - -function isPng(bytes: Uint8Array): boolean { - return ( - bytes.length > 24 && - bytes[0] === 0x89 && - bytes[1] === 0x50 && - bytes[2] === 0x4e && - bytes[3] === 0x47 && - bytes[4] === 0x0d && - bytes[5] === 0x0a && - bytes[6] === 0x1a && - bytes[7] === 0x0a - ); -} - -function readUInt32BE(arr: Uint8Array, offset: number): number { - return ( - ((arr[offset] << 24) >>> 0) + - ((arr[offset + 1] << 16) >>> 0) + - ((arr[offset + 2] << 8) >>> 0) + - (arr[offset + 3] >>> 0) - ); -} - -function pngDimensions(bytes: Uint8Array): { width: number; height: number } | null { - if (!isPng(bytes)) return null; - // PNG IHDR: width/height at offsets 16 and 20 - const width = readUInt32BE(bytes, 16); - const height = readUInt32BE(bytes, 20); - if (!width || !height) return null; - return { width, height }; -} - -function toBase64(bytes: Uint8Array): string { - let binary = ''; - const chunk = 0x8000; - for (let i = 0; i < bytes.length; i += chunk) { - const sub = bytes.subarray(i, i + chunk); - binary += String.fromCharCode(...sub); - } - // btoa is available in Edge runtime - return btoa(binary); -} - -export default async function (req: Request): Promise { - if (req.method !== 'POST') return new Response('Method Not Allowed', { status: 405 }); - try { - const auth = req.headers.get('authorization') || ''; - const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; - if (!token) return new Response(JSON.stringify({ error: 'Missing GitHub token' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); - - const form = await req.formData(); - const target = String(form.get('target') || 'token'); - const globalChainId = String(form.get('chainId') || '').trim(); - const prTitleOverride = String(form.get('prTitle') || '').trim(); - const prBodyOverride = String(form.get('prBody') || '').trim(); - - const prFiles: Array<{ path: string; contentBase64: string }> = []; - const { owner, repo } = resolveTargetRepo(); - - if (target === 'token') { - const addressesRaw = form.getAll('address') as string[]; - const addresses = addressesRaw.map(a => String(a || '').trim()).filter(Boolean); - if (!addresses.length) { - return new Response(JSON.stringify({ error: 'At least one address required for token uploads' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - } - for (let i = 0; i < addresses.length; i++) { - const addr = addresses[i]; - const localChainId = String(form.get(`chainId_${i}`) || globalChainId || '').trim(); - if (!localChainId) return new Response(JSON.stringify({ error: `Missing chainId for token index ${i}` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - - const svgF = form.get(`svg_${i}`) as File | null; - const png32F = form.get(`png32_${i}`) as File | null; - const png128F = form.get(`png128_${i}`) as File | null; - if (!svgF) return new Response(JSON.stringify({ error: `svg_${i} required` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!svgF.type.includes('svg')) return new Response(JSON.stringify({ error: `svg_${i} must be image/svg+xml` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!png32F || !png128F) return new Response(JSON.stringify({ error: `png32_${i} and png128_${i} required` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!png32F.type.includes('png') || !png128F.type.includes('png')) return new Response(JSON.stringify({ error: `png32_${i} and png128_${i} must be image/png` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - - const svgBytes = new Uint8Array(await svgF.arrayBuffer()); - const png32Bytes = new Uint8Array(await png32F.arrayBuffer()); - const png128Bytes = new Uint8Array(await png128F.arrayBuffer()); - - const d32 = pngDimensions(png32Bytes); - const d128 = pngDimensions(png128Bytes); - if (!d32 || d32.width !== 32 || d32.height !== 32) return new Response(JSON.stringify({ error: `png32_${i} must be 32x32` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!d128 || d128.width !== 128 || d128.height !== 128) return new Response(JSON.stringify({ error: `png128_${i} must be 128x128` }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - - const addrLower = addr.toLowerCase(); - prFiles.push( - { path: ['tokens', String(localChainId), addrLower, 'logo.svg'].join('/'), contentBase64: toBase64(svgBytes) }, - { path: ['tokens', String(localChainId), addrLower, 'logo-32.png'].join('/'), contentBase64: toBase64(png32Bytes) }, - { path: ['tokens', String(localChainId), addrLower, 'logo-128.png'].join('/'), contentBase64: toBase64(png128Bytes) }, - ); - } - } else { - // Chain asset mode - if (!globalChainId) return new Response(JSON.stringify({ error: 'chainId required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - const svgF = form.get('svg') as File | null; - const png32F = form.get('png32') as File | null; - const png128F = form.get('png128') as File | null; - if (!svgF) return new Response(JSON.stringify({ error: 'svg required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!svgF.type.includes('svg')) return new Response(JSON.stringify({ error: 'svg must be image/svg+xml' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!png32F || !png128F) return new Response(JSON.stringify({ error: 'png32 and png128 required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!png32F.type.includes('png') || !png128F.type.includes('png')) return new Response(JSON.stringify({ error: 'png32 and png128 must be image/png' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - - const svgBytes = new Uint8Array(await svgF.arrayBuffer()); - const png32Bytes = new Uint8Array(await png32F.arrayBuffer()); - const png128Bytes = new Uint8Array(await png128F.arrayBuffer()); - - const d32 = pngDimensions(png32Bytes); - const d128 = pngDimensions(png128Bytes); - if (!d32 || d32.width !== 32 || d32.height !== 32) return new Response(JSON.stringify({ error: 'png32 must be 32x32' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - if (!d128 || d128.width !== 128 || d128.height !== 128) return new Response(JSON.stringify({ error: 'png128 must be 128x128' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); - - prFiles.push( - { path: ['chains', String(globalChainId), 'logo.svg'].join('/'), contentBase64: toBase64(svgBytes) }, - { path: ['chains', String(globalChainId), 'logo-32.png'].join('/'), contentBase64: toBase64(png32Bytes) }, - { path: ['chains', String(globalChainId), 'logo-128.png'].join('/'), contentBase64: toBase64(png128Bytes) }, - ); - } - - const login = await getUserLogin(token).catch(() => 'user'); - const branchName = `${login}-image-tools-${target}-${Date.now()}`; - - // Build default PR title/body if not provided - let prTitle = prTitleOverride; - let prBody = prBodyOverride; - if (!prTitle || !prBody) { - if (target === 'token') { - const addressesForBody = (form.getAll('address') as string[]).map(a => a?.toLowerCase?.() || a).filter(Boolean); - const chainsForBody: string[] = addressesForBody.map((_, i) => String(form.get(`chainId_${i}`) || globalChainId || '')); - const uniqueChains = Array.from(new Set(chainsForBody.filter(Boolean))); - prTitle ||= `feat: add token assets (${addressesForBody.length})`; - const directoryLocations = addressesForBody.flatMap((addr: string, i: number) => [ - `/token/${chainsForBody[i]}/${addr}/logo.svg`, - `/token/${chainsForBody[i]}/${addr}/logo-32.png`, - `/token/${chainsForBody[i]}/${addr}/logo-128.png`, - ]); - prBody ||= [ - `Chains: ${uniqueChains.join(', ')}`, - `Addresses: ${addressesForBody.join(', ')}`, - '', - 'Uploaded locations:', - ...directoryLocations.map((u) => `- ${u}`), - ].join('\n'); - } else { - prTitle ||= `feat: add chain assets on ${globalChainId}`; - const directoryLocations = [`/chain/${globalChainId}/logo.svg`, `/chain/${globalChainId}/logo-32.png`, `/chain/${globalChainId}/logo-128.png`]; - prBody ||= [`Chain: ${globalChainId}`, '', 'Uploaded locations:', ...directoryLocations.map((u) => `- ${u}`)].join('\n'); - } - } - - const prUrl = await openPrWithFilesForkAware({ - token, - baseOwner: owner, - baseRepo: repo, - branchName, - commitMessage: prTitle, - prTitle, - prBody, - files: prFiles, - }); - - return new Response(JSON.stringify({ ok: true, prUrl }), { status: 200, headers: { 'Content-Type': 'application/json' } }); - } catch (e: any) { - return new Response(JSON.stringify({ error: e?.message || 'Upload failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); - } -} diff --git a/app/image-tools/postcss.config.cjs b/app/image-tools/postcss.config.cjs deleted file mode 100644 index c21c076356..0000000000 --- a/app/image-tools/postcss.config.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; - diff --git a/app/image-tools/src/components/GithubSignIn.tsx b/app/image-tools/src/components/GithubSignIn.tsx deleted file mode 100644 index ca48ec3b2c..0000000000 --- a/app/image-tools/src/components/GithubSignIn.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, {Fragment, useEffect, useState} from 'react'; -import {Dialog, Transition} from '@headlessui/react'; - -import { - broadcastAuthChange, - readStoredToken, - storeAuthState, - clearStoredAuth, - TOKEN_STORAGE_KEY, - AUTH_CHANGE_EVENT, - buildAuthorizeUrl, - markAuthPending, - clearAuthPending, - readAuthPending -} from '../lib/githubAuth'; - -function randomState(len = 20) { - const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; - let out = ''; - for (let i = 0; i < len; i++) out += chars[Math.floor(Math.random() * chars.length)]; - return out; -} - -export const GithubSignIn: React.FC = () => { - const [token, setToken] = useState(null); - const [connecting, setConnecting] = useState(() => readAuthPending()); - const [login, setLogin] = useState(null); - - useEffect(() => { - if (typeof window === 'undefined') return; - - const syncState = () => { - setToken(readStoredToken()); - setConnecting(readAuthPending()); - }; - syncState(); - - const onStorage = (event: StorageEvent) => { - if (!event.key || event.key === TOKEN_STORAGE_KEY) syncState(); - }; - const onAuthEvent = () => syncState(); - window.addEventListener('storage', onStorage); - window.addEventListener(AUTH_CHANGE_EVENT, onAuthEvent); - return () => { - window.removeEventListener('storage', onStorage); - window.removeEventListener(AUTH_CHANGE_EVENT, onAuthEvent); - }; - }, []); - - useEffect(() => { - if (!token) { - setLogin(null); - setConnecting(false); - return; - } - let cancelled = false; - fetch('https://api.github.com/user', { - headers: {Authorization: `Bearer ${token}`} - }) - .then(r => (r.ok ? r.json() : null)) - .then(j => { - if (!cancelled) setLogin(j?.login ?? null); - }) - .catch(() => { - if (!cancelled) setLogin(null); - }); - return () => { - cancelled = true; - }; - }, [token]); - - const signIn = () => { - const state = randomState(); - storeAuthState(state); - markAuthPending(); - setConnecting(true); - const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID; - if (!clientId) { - alert('Missing VITE_GITHUB_CLIENT_ID'); - return; - } - window.location.href = buildAuthorizeUrl(clientId, state); - }; - - const signOut = () => { - clearStoredAuth(); - clearAuthPending(); - setToken(null); - setLogin(null); - setConnecting(false); - broadcastAuthChange(); - }; - - if (token) { - return ( - - ); - } - - return ( - <> - - - { - clearAuthPending(); - setConnecting(false); - }}> - - - - - ); -}; diff --git a/app/image-tools/src/components/Header.tsx b/app/image-tools/src/components/Header.tsx deleted file mode 100644 index e832257e0e..0000000000 --- a/app/image-tools/src/components/Header.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { GithubSignIn } from './GithubSignIn'; -import { AUTH_CHANGE_EVENT, TOKEN_STORAGE_KEY, readStoredToken } from '../lib/githubAuth'; - -export const Header: React.FC = () => { - const [token, setToken] = useState(null); - useEffect(() => { - if (typeof window === 'undefined') return; - const update = () => setToken(readStoredToken()); - update(); - const onStorage = (event: StorageEvent) => { - if (!event.key || event.key === TOKEN_STORAGE_KEY) update(); - }; - const onAuth = () => update(); - window.addEventListener('storage', onStorage); - window.addEventListener(AUTH_CHANGE_EVENT, onAuth); - return () => { - window.removeEventListener('storage', onStorage); - window.removeEventListener(AUTH_CHANGE_EVENT, onAuth); - }; - }, []); - - return ( -
-
-

Yearn Asset Repo Upload

- -
-
- ); -}; diff --git a/app/image-tools/src/components/SegmentedToggle.tsx b/app/image-tools/src/components/SegmentedToggle.tsx deleted file mode 100644 index e6efd49641..0000000000 --- a/app/image-tools/src/components/SegmentedToggle.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -type Option = { value: string; label: string }; - -export function SegmentedToggle({ - options, - value, - onChange, - className, -}: { - options: Option[]; - value: string; - onChange: (v: string) => void; - className?: string; -}) { - const selectedIndex = Math.max(0, options.findIndex((o) => o.value === value)); - return ( -
-
- {/* Slider */} -
- {options.map((opt) => { - const active = opt.value === value; - return ( - - ); - })} -
-
- ); -} diff --git a/app/image-tools/src/lib/api.ts b/app/image-tools/src/lib/api.ts deleted file mode 100644 index e8110d5679..0000000000 --- a/app/image-tools/src/lib/api.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Resolve a safe absolute base URL for API calls. -// In production on Vercel, keep VITE_API_BASE_URL unset (or '/'), and we will use the current origin. -const RAW_BASE = (import.meta as any).env.VITE_API_BASE_URL as string | undefined; -export const API_BASE_URL = (() => { - // Prefer explicit absolute URL if provided. - if (RAW_BASE && RAW_BASE !== '/' && /^https?:\/\//i.test(RAW_BASE)) return RAW_BASE; - // Otherwise, fall back to current origin in the browser. - if (typeof window !== 'undefined' && window.location?.origin) return window.location.origin; - // Last resort for non-browser contexts. - return 'http://localhost'; -})(); - -export async function apiFetch(path: string, init?: RequestInit) { - const url = new URL(path, API_BASE_URL); - const res = await fetch(url.toString(), init); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} diff --git a/app/image-tools/src/lib/chains.ts b/app/image-tools/src/lib/chains.ts deleted file mode 100644 index 9b39dd87d4..0000000000 --- a/app/image-tools/src/lib/chains.ts +++ /dev/null @@ -1,44 +0,0 @@ -export const CHAIN_ID_TO_NAME: Record = { - 1: 'Ethereum', - 10: 'Optimism', - 100: 'GnosisChain', - 137: 'Polygon', - 146: 'Sonic', - 250: 'Fantom', - 8453: 'Base', - 42161: 'Arbitrum', - 747474: 'Katana', - 80094: 'Berachain', -}; - -// Optional built-in public RPCs for convenience; env can override. -const DEFAULT_RPCS: Partial> = { - 1: 'https://cloudflare-eth.com', - 10: 'https://mainnet.optimism.io', - 100: 'https://rpc.gnosischain.com', - 137: 'https://polygon-rpc.com', - 250: 'https://rpc.ankr.com/fantom', - 42161: 'https://arb1.arbitrum.io/rpc', - 8453: 'https://mainnet.base.org', - // 146, 747474, 80094 intentionally omitted without known public RPCs -}; - -export function getRpcUrl(chainId: number): string | undefined { - // Prefer explicit env overrides - const env = (import.meta as any).env || {}; - const k1 = `VITE_RPC_URI_FOR_${chainId}`; - const k2 = `VITE_RPC_${chainId}`; - const fromEnv = (env[k1] as string | undefined) || (env[k2] as string | undefined); - if (fromEnv) return fromEnv; - return DEFAULT_RPCS[chainId]; -} - -export function listKnownChains(): Array<{ id: number; name: string }> { - return Object.entries(CHAIN_ID_TO_NAME) - .map(([id, name]) => ({ id: Number(id), name })) - .sort((a, b) => a.id - b.id); -} - -export function isEvmAddress(addr: string): boolean { - return /^0x[a-fA-F0-9]{40}$/.test(addr.trim()); -} diff --git a/app/image-tools/src/lib/githubAuth.ts b/app/image-tools/src/lib/githubAuth.ts deleted file mode 100644 index 5307f9ba0b..0000000000 --- a/app/image-tools/src/lib/githubAuth.ts +++ /dev/null @@ -1,90 +0,0 @@ -export const TOKEN_STORAGE_KEY = 'github_token'; -export const AUTH_STATE_STORAGE_KEY = 'auth_state'; -export const AUTH_CHANGE_EVENT = 'github-auth-changed'; -export const AUTH_PENDING_STORAGE_KEY = 'github_oauth_pending'; - -export function buildAuthorizeUrl(clientId: string, state: string) { - const url = new URL('https://github.com/login/oauth/authorize'); - url.searchParams.set('client_id', clientId); - url.searchParams.set('state', state); - url.searchParams.set('scope', 'public_repo'); - return url.toString(); -} - -export function readStoredToken(): string | null { - if (typeof window === 'undefined') return null; - try { - return sessionStorage.getItem(TOKEN_STORAGE_KEY); - } catch { - return null; - } -} - -export function storeAuthState(state: string) { - if (typeof window === 'undefined') return; - try { - sessionStorage.setItem(AUTH_STATE_STORAGE_KEY, state); - } catch {} -} - -export function readStoredState(): string | null { - if (typeof window === 'undefined') return null; - try { - return sessionStorage.getItem(AUTH_STATE_STORAGE_KEY); - } catch { - return null; - } -} - -export function storeAuthToken(token: string) { - if (typeof window === 'undefined') return; - try { - sessionStorage.setItem(TOKEN_STORAGE_KEY, token); - } catch {} -} - -export function clearStoredState() { - if (typeof window === 'undefined') return; - try { - sessionStorage.removeItem(AUTH_STATE_STORAGE_KEY); - } catch {} -} - -export function clearStoredAuth() { - if (typeof window === 'undefined') return; - try { - sessionStorage.removeItem(TOKEN_STORAGE_KEY); - sessionStorage.removeItem(AUTH_STATE_STORAGE_KEY); - } catch {} -} - -export function readAuthPending(): boolean { - if (typeof window === 'undefined') return false; - try { - return sessionStorage.getItem(AUTH_PENDING_STORAGE_KEY) === 'true'; - } catch { - return false; - } -} - -export function markAuthPending() { - if (typeof window === 'undefined') return; - try { - sessionStorage.setItem(AUTH_PENDING_STORAGE_KEY, 'true'); - } catch {} -} - -export function clearAuthPending() { - if (typeof window === 'undefined') return; - try { - sessionStorage.removeItem(AUTH_PENDING_STORAGE_KEY); - } catch {} -} - -export function broadcastAuthChange() { - if (typeof window === 'undefined') return; - window.dispatchEvent(new Event(AUTH_CHANGE_EVENT)); - try { - window.dispatchEvent(new StorageEvent('storage', { key: TOKEN_STORAGE_KEY })); - } catch {} -} diff --git a/app/image-tools/src/main.tsx b/app/image-tools/src/main.tsx deleted file mode 100644 index 9995568f86..0000000000 --- a/app/image-tools/src/main.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { RouterProvider } from '@tanstack/react-router'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { router } from './router'; -import './index.css'; - -const queryClient = new QueryClient(); - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - -); diff --git a/app/image-tools/src/router.tsx b/app/image-tools/src/router.tsx deleted file mode 100644 index aa71be4720..0000000000 --- a/app/image-tools/src/router.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router'; -import React from 'react'; -import { UploadRoute } from './routes/upload'; -import { GithubSuccessRoute } from './routes/auth/github-success'; -import { Header } from './components/Header'; - -const RootComponent: React.FC = () => { - return ( -
-
-
- -
-
- ); -}; - -export const rootRoute = createRootRoute({ - component: RootComponent, -}); - -const routeTree = rootRoute.addChildren([UploadRoute, GithubSuccessRoute]); - -export const router = createRouter({ routeTree }); - -declare module '@tanstack/react-router' { - interface Register { - router: typeof router; - } -} diff --git a/app/image-tools/src/routes/auth/github-success.tsx b/app/image-tools/src/routes/auth/github-success.tsx deleted file mode 100644 index 7ac6abd95b..0000000000 --- a/app/image-tools/src/routes/auth/github-success.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useEffect } from 'react'; -import { createRoute, useNavigate } from '@tanstack/react-router'; -import { rootRoute } from '../../router'; -import { broadcastAuthChange, clearAuthPending, clearStoredState, readStoredState, storeAuthToken } from '../../lib/githubAuth'; - -const GithubSuccessComponent: React.FC = () => { - const navigate = useNavigate(); - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const token = params.get('token'); - const state = params.get('state'); - const returnedState = readStoredState(); - if (state && returnedState && state !== returnedState) { - console.warn('OAuth state mismatch.'); - } - clearAuthPending(); - if (token) { - storeAuthToken(token); - broadcastAuthChange(); - } - if (state) clearStoredState(); - navigate({ to: '/' }); - }, [navigate]); - - return

Signing you in…

; -}; - -export const GithubSuccessRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/auth/github/success', - component: GithubSuccessComponent, -}); diff --git a/app/image-tools/src/routes/upload.tsx b/app/image-tools/src/routes/upload.tsx deleted file mode 100644 index e2ec95a96a..0000000000 --- a/app/image-tools/src/routes/upload.tsx +++ /dev/null @@ -1,1139 +0,0 @@ -import React, {Fragment, useEffect, useMemo, useState} from 'react'; -import {createRoute} from '@tanstack/react-router'; -import {rootRoute} from '../router'; -import {GithubSignIn} from '../components/GithubSignIn'; -import {API_BASE_URL} from '../lib/api'; -import {Dialog, Switch, Transition} from '@headlessui/react'; -import {getRpcUrl, isEvmAddress, listKnownChains} from '../lib/chains'; -import {SegmentedToggle} from '../components/SegmentedToggle'; -import {AUTH_CHANGE_EVENT, TOKEN_STORAGE_KEY, readStoredToken} from '../lib/githubAuth'; - -type TokenItem = { - chainId: string; - address: string; - name?: string; - genPng: boolean; - files: {svg?: File; png32?: File; png128?: File}; - preview: {svg?: string; png32?: string; png128?: string}; - resolvingName?: boolean; - resolveError?: string; - addressValid?: boolean; -}; - -export const UploadComponent: React.FC = () => { - const [token, setToken] = useState(() => readStoredToken()); - const [mode, setMode] = useState<'token' | 'chain'>('token'); - const [chainId, setChainId] = useState(''); - const [chainGenPng, setChainGenPng] = useState(true); - // Chain single - const [chainFiles, setChainFiles] = useState<{svg?: File; png32?: File; png128?: File}>({}); - const [chainPreview, setChainPreview] = useState<{svg?: string; png32?: string; png128?: string}>({}); - // Token items - const [tokenItems, setTokenItems] = useState([ - {chainId: '', address: '', name: '', genPng: true, files: {}, preview: {}} - ]); - - // PR review modal state - const [reviewOpen, setReviewOpen] = useState(false); - const [prTitle, setPrTitle] = useState(''); - const [prBody, setPrBody] = useState(''); - const [submitting, setSubmitting] = useState(false); - - const canSubmit = useMemo(() => { - if (mode === 'chain') { - if (!chainId) return false; - if (!chainFiles.svg) return false; - if (!chainGenPng && (!chainFiles.png32 || !chainFiles.png128)) return false; - return true; - } - if (!tokenItems.length) return false; - for (const it of tokenItems) { - if (!it.chainId || !it.address || !it.files.svg) return false; - if (!it.genPng && (!it.files.png32 || !it.files.png128)) return false; - } - return true; - }, [chainId, mode, chainFiles, tokenItems, chainGenPng]); - - useEffect(() => { - if (typeof window === 'undefined') return; - const update = () => setToken(readStoredToken()); - const handler = (event: StorageEvent) => { - if (!event.key || event.key === TOKEN_STORAGE_KEY) update(); - }; - const onAuth = () => update(); - window.addEventListener('storage', handler); - window.addEventListener(AUTH_CHANGE_EVENT, onAuth); - return () => { - window.removeEventListener('storage', handler); - window.removeEventListener(AUTH_CHANGE_EVENT, onAuth); - }; - }, []); - - // ---- Helpers: Token name lookup via JSON-RPC ---- - async function fetchErc20Name(chainIdStr: string, address: string): Promise { - const cid = Number(chainIdStr); - if (!cid || Number.isNaN(cid)) throw new Error('Invalid chain'); - // Prefer server endpoint to avoid CORS and centralize env - try { - const url = new URL('/api/erc20-name', API_BASE_URL).toString(); - const res = await fetch(url, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({chainId: cid, address}) - }); - if (!res.ok) throw new Error(await res.text()); - const j = await res.json(); - if (j?.name) return j.name as string; - } catch (e) { - // fall through to direct RPC attempt - } - const rpc = getRpcUrl(cid); - if (!rpc) throw new Error('No RPC configured for this chain'); - const data = '0x06fdde03'; - const payload = { - jsonrpc: '2.0', - id: Math.floor(Math.random() * 1e9), - method: 'eth_call', - params: [{to: address, data}, 'latest'] - }; - const res = await fetch(rpc, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(payload) - }); - if (!res.ok) throw new Error(`RPC ${res.status}`); - const json = await res.json(); - if (json?.error) throw new Error(json.error?.message || 'RPC error'); - const result: string | undefined = json?.result; - if (!result || result === '0x') throw new Error('Empty result'); - return decodeAbiString(result); - } - - function decodeAbiString(resultHex: string): string { - const hex = resultHex.startsWith('0x') ? resultHex.slice(2) : resultHex; - // Dynamic string encoding: offset (32 bytes), length (32 bytes), data - if (hex.length >= 192) { - const lenHex = hex.slice(64, 128); - const len = parseInt(lenHex || '0', 16); - const dataHex = hex.slice(128, 128 + len * 2); - return hexToUtf8(dataHex); - } - // Fallback: bytes32-like (padded) - if (hex.length === 64) { - return hexToUtf8(hex.replace(/00+$/, '')); - } - // Last resort: try to interpret whatever is there - return hexToUtf8(hex); - } - - function hexToUtf8(hex: string): string { - const bytes = hex.match(/.{1,2}/g)?.map(b => parseInt(b, 16)) || []; - return new TextDecoder().decode(new Uint8Array(bytes)).replace(/\u0000+$/, ''); - } - - const onChainFileChange = (e: React.ChangeEvent) => { - const f = e.target.files?.[0]; - if (!f) return; - const name = e.target.name as 'svg' | 'png32' | 'png128'; - setChainFiles(prev => ({...prev, [name]: f})); - const url = URL.createObjectURL(f); - setChainPreview(p => ({...p, [name]: url}) as any); - }; - - const onTokenFileChange = (index: number, e: React.ChangeEvent) => { - const f = e.target.files?.[0]; - if (!f) return; - const name = e.target.name as 'svg' | 'png32' | 'png128'; - setTokenItems(prev => { - const arr = [...prev]; - const item = {...arr[index]}; - item.files = {...item.files, [name]: f}; - const url = URL.createObjectURL(f); - item.preview = {...item.preview, [name]: url} as any; - arr[index] = item; - return arr; - }); - }; - - // Generate PNG previews for chain when requested - useEffect(() => { - async function makePngPreviews(svgFile: File) { - const svgUrl = URL.createObjectURL(svgFile); - const img = new Image(); - img.src = svgUrl; - await img.decode().catch(() => {}); - const gen = async (size: number) => { - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - if (!ctx) return ''; - ctx.clearRect(0, 0, size, size); - const scale = Math.min(size / img.width, size / img.height); - const w = img.width * scale; - const h = img.height * scale; - const x = (size - w) / 2; - const y = (size - h) / 2; - ctx.drawImage(img, x, y, w, h); - return canvas.toDataURL('image/png'); - }; - const p32 = await gen(32); - const p128 = await gen(128); - setChainPreview(p => ({...p, png32: p32, png128: p128})); - URL.revokeObjectURL(svgUrl); - } - if (mode === 'chain' && chainGenPng && chainFiles.svg) { - makePngPreviews(chainFiles.svg); - } - }, [chainGenPng, chainFiles.svg, mode]); - - // Generate PNG previews for each token item - useEffect(() => { - if (mode !== 'token') return; - tokenItems.forEach(async (it, idx) => { - if (!it.genPng || !it.files.svg) return; - const svgUrl = URL.createObjectURL(it.files.svg); - const img = new Image(); - img.src = svgUrl; - await img.decode().catch(() => {}); - const gen = async (size: number) => { - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - if (!ctx) return ''; - ctx.clearRect(0, 0, size, size); - const scale = Math.min(size / img.width, size / img.height); - const w = img.width * scale; - const h = img.height * scale; - const x = (size - w) / 2; - const y = (size - h) / 2; - ctx.drawImage(img, x, y, w, h); - return canvas.toDataURL('image/png'); - }; - const p32 = await gen(32); - const p128 = await gen(128); - setTokenItems(prev => { - const arr = [...prev]; - const cur = arr[idx]; - if (cur.preview.png32 === p32 && cur.preview.png128 === p128) return prev; - arr[idx] = {...cur, preview: {...cur.preview, png32: p32, png128: p128}}; - return arr; - }); - URL.revokeObjectURL(svgUrl); - }); - }, [ - JSON.stringify( - tokenItems.map(i => ({ - svg: i.files.svg ? i.files.svg.name + ':' + i.files.svg.lastModified : '', - gen: i.genPng - })) - ), - mode - ]); - - function buildDefaultPrMetadata() { - if (mode === 'token') { - const addressesForBody = tokenItems.map(i => (i.address?.toLowerCase?.() as string) || '').filter(Boolean); - const chainsForBody = tokenItems.map(i => i.chainId || chainId || '').map(String); - const uniqueChains = Array.from(new Set(chainsForBody.filter(Boolean))); - const title = `feat: add token assets (${addressesForBody.length})`; - const directoryLocations = addressesForBody.flatMap((addr, i) => [ - `/token/${chainsForBody[i]}/${addr}/logo.svg`, - `/token/${chainsForBody[i]}/${addr}/logo-32.png`, - `/token/${chainsForBody[i]}/${addr}/logo-128.png` - ]); - const body = [ - `Chains: ${uniqueChains.join(', ')}`, - `Addresses: ${addressesForBody.join(', ')}`, - '', - 'Directory Location of uploaded logos:', - ...directoryLocations.map(u => `- ${u}`) - ].join('\n'); - return {title, body}; - } else { - const title = `feat: add chain assets on ${chainId}`; - const sampleUrls = [ - `/chain/${chainId}/logo.svg`, - `/chain/${chainId}/logo-32.png`, - `/chain/${chainId}/logo-128.png` - ]; - const body = [ - `Chain: ${chainId}`, - '', - 'Directory Location of uploaded logos:', - ...sampleUrls.map(u => `- ${u}`) - ].join('\n'); - return {title, body}; - } - } - - async function dataUrlToFile(dataUrl: string, filename: string, type = 'image/png'): Promise { - const res = await fetch(dataUrl); - const blob = await res.blob(); - return new File([blob], filename, {type}); - } - - async function buildFormData(withPrMeta?: {title: string; body: string}) { - const body = new FormData(); - body.append('target', mode); - body.append('chainId', chainId); - if (mode === 'token') { - for (let i = 0; i < tokenItems.length; i++) { - const it = tokenItems[i]; - body.append(`chainId_${i}`, it.chainId); - if (it.address) body.append('address', it.address); - if (it.files.svg) body.append(`svg_${i}`, it.files.svg); - // Ensure PNGs are attached; if genPng is true and files are missing, use previews to create PNGs client-side. - const needPng32 = !it.files.png32 && !!it.preview.png32; - const needPng128 = !it.files.png128 && !!it.preview.png128; - if (needPng32) body.append(`png32_${i}`, await dataUrlToFile(it.preview.png32!, 'logo-32.png')); - if (needPng128) body.append(`png128_${i}`, await dataUrlToFile(it.preview.png128!, 'logo-128.png')); - // Also include any user-provided PNGs - if (it.files.png32) body.append(`png32_${i}`, it.files.png32); - if (it.files.png128) body.append(`png128_${i}`, it.files.png128); - } - } else { - if (chainFiles.svg) body.append('svg', chainFiles.svg); - // Ensure chain PNGs are attached; generate from previews if needed. - const needP32 = !chainFiles.png32 && !!chainPreview.png32; - const needP128 = !chainFiles.png128 && !!chainPreview.png128; - if (needP32) body.append('png32', await dataUrlToFile(chainPreview.png32!, 'logo-32.png')); - if (needP128) body.append('png128', await dataUrlToFile(chainPreview.png128!, 'logo-128.png')); - if (chainFiles.png32) body.append('png32', chainFiles.png32); - if (chainFiles.png128) body.append('png128', chainFiles.png128); - } - if (withPrMeta) { - body.append('prTitle', withPrMeta.title); - body.append('prBody', withPrMeta.body); - } - return body; - } - - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!token) return alert('Sign in with GitHub first.'); - const {title, body} = buildDefaultPrMetadata(); - setPrTitle(title); - setPrBody(body); - setReviewOpen(true); - }; - - async function confirmAndSubmit() { - if (!token) return alert('Sign in with GitHub first.'); - setSubmitting(true); - try { - const reqUrl = new URL('/api/upload', API_BASE_URL).toString(); - const form = await buildFormData({title: prTitle, body: prBody}); - const res = await fetch(reqUrl, {method: 'POST', headers: {Authorization: `Bearer ${token}`}, body: form}); - if (!res.ok) { - const ct = res.headers.get('content-type') || ''; - let msg = ''; - try { - if (ct.includes('application/json')) { - const j = await res.json(); - msg = j?.error || JSON.stringify(j); - } else { - msg = await res.text(); - } - } catch {} - alert(`Upload failed: ${msg || res.status}`); - return; - } - const json = await res.json(); - if (json?.prUrl) { - window.open(json.prUrl, '_blank'); - } else { - alert('Upload complete, PR created.'); - } - // Reset form state after successful PR creation - setReviewOpen(false); - setTokenItems([{chainId: '', address: '', name: '', genPng: true, files: {}, preview: {}}]); - setChainId(''); - setChainGenPng(true); - setChainFiles({}); - setChainPreview({}); - } finally { - setSubmitting(false); - } - } - - return ( -
-
- {/* First asset card */} -
-
-

First asset to add

-
- - setMode(v as 'token' | 'chain')} - /> -
-
-
- - {mode === 'token' && ( - <> - - - - )} - {/* SVG input row */} -
- {/* Drop area */} -
-
e.preventDefault()} - onDrop={e => { - e.preventDefault(); - const f = e.dataTransfer.files?.[0]; - if (!f) return; - if (mode === 'token') { - onTokenFileChange(0, {target: {files: [f], name: 'svg'}} as any); - } else { - onChainFileChange({target: {files: [f], name: 'svg'}} as any); - } - }} - className="flex h-40 w-full items-center justify-center rounded-md border-2 border-dashed border-gray-300 bg-gray-50 text-sm text-gray-600"> - {mode === 'token' ? ( - tokenItems[0]?.preview.svg ? ( - - ) : ( - Drag & Drop SVG here - ) - ) : chainPreview.svg ? ( - - ) : ( - Drag & Drop SVG here - )} -
-
- {/* Extras column */} -
-
- Generate PNGs - { - if (mode === 'token') - setTokenItems(prev => [{...prev[0], genPng: v}, ...prev.slice(1)]); - else setChainGenPng(v); - }} - className={`${ - (mode === 'token' ? tokenItems[0]?.genPng : chainGenPng) - ? 'bg-blue-600' - : 'bg-gray-200' - } relative inline-flex h-6 w-11 items-center rounded-full transition`}> - - -
-
- -
- {!(mode === 'token' ? tokenItems[0]?.genPng : chainGenPng) && ( -
- - - mode === 'token' - ? onTokenFileChange(0, { - ...e, - target: {...e.target, name: 'png32'} - } as any) - : onChainFileChange({ - ...e, - target: {...e.target, name: 'png32'} - } as any) - } - /> - - - mode === 'token' - ? onTokenFileChange(0, { - ...e, - target: {...e.target, name: 'png128'} - } as any) - : onChainFileChange({ - ...e, - target: {...e.target, name: 'png128'} - } as any) - } - /> -
- )} -
-
- {/* Previews for first card */} -
-

Previews

-
-
-

SVG

-
- {(mode === 'token' ? tokenItems[0]?.preview.svg : chainPreview.svg) ? ( - svg - ) : ( - - )} -
-
-
-

PNG 32x32

-
- {(mode === 'token' ? tokenItems[0]?.preview.png32 : chainPreview.png32) ? ( - png32 - ) : ( - - )} -
-
-
-

PNG 128x128

-
- {(mode === 'token' ? tokenItems[0]?.preview.png128 : chainPreview.png128) ? ( - png128 - ) : ( - - )} -
-
-
-
-
-
- - {/* Additional token asset cards */} - {mode === 'token' && - tokenItems.slice(1).map((it, idx) => ( -
-
-
Token Asset #{idx + 2}
-
- - -
-
-
- - - -
-
-
-
e.preventDefault()} - onDrop={e => { - e.preventDefault(); - const f = e.dataTransfer.files?.[0]; - if (!f) return; - onTokenFileChange(idx + 1, {target: {files: [f], name: 'svg'}} as any); - }} - className="flex h-40 w-full items-center justify-center rounded-md border-2 border-dashed border-gray-300 bg-gray-50 text-sm text-gray-600"> - {it.preview.svg ? ( - - ) : ( - Drag & Drop SVG here - )} -
-
-
-
- Generate PNGs - - setTokenItems(prev => - prev.map((x, i) => (i === idx + 1 ? {...x, genPng: v} : x)) - ) - } - className={`${ - it.genPng ? 'bg-blue-600' : 'bg-gray-200' - } relative inline-flex h-6 w-11 items-center rounded-full transition`}> - - -
- - {!it.genPng && ( -
- - - onTokenFileChange(idx + 1, { - ...e, - target: {...e.target, name: 'png32'} - } as any) - } - /> - - - onTokenFileChange(idx + 1, { - ...e, - target: {...e.target, name: 'png128'} - } as any) - } - /> -
- )} -
-
- {/* Previews for additional token card */} -
-

Previews

-
-
-

SVG

-
- {it.preview.svg ? ( - svg - ) : ( - - )} -
-
-
-

PNG 32x32

-
- {it.preview.png32 ? ( - png32 - ) : ( - - )} -
-
-
-

PNG 128x128

-
- {it.preview.png128 ? ( - png128 - ) : ( - - )} -
-
-
-
-
- ))} - - {mode === 'token' && ( -
- -
- )} - -
- -
-
- - {/* PR Review Modal */} - - (submitting ? null : setReviewOpen(false))}> -