From e8748e2dde5bfe162a5e957a9c15b9a766c5d13f Mon Sep 17 00:00:00 2001 From: rossgalloway <58150151+rossgalloway@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:56:30 +0000 Subject: [PATCH 1/2] update auth callback and runtime --- app/image-tools/README.md | 17 +-- app/image-tools/api/auth/github/callback.ts | 19 +++- app/image-tools/api/auth/github/start.ts | 32 ++++++ app/image-tools/api/upload.ts | 7 +- .../src/components/GithubSignIn.tsx | 10 +- app/image-tools/src/lib/githubAuth.ts | 13 ++- .../src/routes/auth/github-success.tsx | 56 +++++---- .../src/server/githubOAuthState.ts | 107 ++++++++++++++++++ app/image-tools/vercel.json | 11 +- app/image-tools/vite.config.ts | 6 +- 10 files changed, 219 insertions(+), 59 deletions(-) create mode 100644 app/image-tools/api/auth/github/start.ts create mode 100644 app/image-tools/src/server/githubOAuthState.ts diff --git a/app/image-tools/README.md b/app/image-tools/README.md index 7b4a6a743c..23849e0955 100644 --- a/app/image-tools/README.md +++ b/app/image-tools/README.md @@ -5,20 +5,21 @@ A lightweight SPA + Vercel Functions app for uploading token/chain assets and op ## Environment Variables (Dev/Prod) - Client (exposed to browser) - - `VITE_GITHUB_CLIENT_ID` — GitHub OAuth App client ID. + - `VITE_GITHUB_OAUTH_BROKER_ORIGIN` — optional; defaults to `https://token-assets.yearn.fi`, whose registered + callback brokers sign-in back to approved preview origins. - `VITE_API_BASE_URL` — optional; default same-origin. Set only if the API lives on another origin. - `VITE_RPC_URI_FOR_` — optional RPC URLs used by `/api/erc20-name`. - Server (Vercel Functions) - - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — same GitHub OAuth App used by the client. - - `APP_BASE_URL` — optional; default request origin. Only set if SPA and API are on different origins. For the - public deployment, leave unset or set to `https://token-assets.yearn.fi`; do not point this at a Vercel alias. + - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — GitHub OAuth App credentials used by the production broker. + - `OAUTH_RETURN_ORIGINS` — optional comma-separated list of additional exact origins allowed after GitHub OAuth. + The canonical production origin and the private `dev-vm.tail197cc7.ts.net` preview host are already trusted. - `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. - GitHub OAuth App callback must be configured to the deployed API callback URL: `https:///api/auth/github/callback`. The client intentionally does not send a `redirect_uri`; GitHub - uses the callback URL registered on the OAuth App, and the API redirects back to `APP_BASE_URL` after exchanging the - code. + uses the callback URL registered on the OAuth App. The signed OAuth state then safely returns the user to the + production or approved preview origin that started sign-in. ## Commands @@ -31,10 +32,10 @@ A lightweight SPA + Vercel Functions app for uploading token/chain assets and op ## 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. +2. Sign in with GitHub — Browser starts at the production OAuth broker, which signs the requested app origin into the OAuth state before redirecting to GitHub. GitHub always returns to the registered production `/api/auth/github/callback` (Edge), which verifies the state, exchanges the code, and safely returns to `/auth/github/success` on the original production or preview origin. 3. Enter chain/address — Client may call `POST /api/erc20-name` (Edge) to resolve ERC‑20 name. 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. +5. Submit PR — Client posts multipart form to `POST /api/upload` (Node.js) with `svg`, `png32`, and `png128`. The function validates sizes and opens a PR via GitHub API. GitHub's tree API creates missing token or chain directories from the submitted file paths. ## Notes diff --git a/app/image-tools/api/auth/github/callback.ts b/app/image-tools/api/auth/github/callback.ts index 152e82f408..a0f13c1281 100644 --- a/app/image-tools/api/auth/github/callback.ts +++ b/app/image-tools/api/auth/github/callback.ts @@ -1,5 +1,7 @@ export const config = {runtime: 'edge'}; +import {readOAuthStateWithLegacyProductionFallback, resolveOAuthReturnTo} from '../../../src/server/githubOAuthState'; + export default async function (req: Request): Promise { try { const url = new URL(req.url); @@ -20,6 +22,17 @@ export default async function (req: Request): Promise { headers: {'Content-Type': 'application/json'} }); } + let oauthState: Awaited>; + let returnTo: string; + try { + oauthState = await readOAuthStateWithLegacyProductionFallback(state, clientSecret); + returnTo = resolveOAuthReturnTo(oauthState.returnTo, process.env.OAUTH_RETURN_ORIGINS || ''); + } catch (error: any) { + return new Response(JSON.stringify({error: error?.message || 'Invalid OAuth state'}), { + status: 400, + headers: {'Content-Type': 'application/json'} + }); + } const tokenRes = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', @@ -43,10 +56,8 @@ export default async function (req: Request): Promise { }); } - 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); + const redirect = new URL('/auth/github/success', returnTo); + redirect.hash = new URLSearchParams({token: accessToken, state: oauthState.nonce}).toString(); return Response.redirect(redirect.toString(), 302); } catch (e: any) { return new Response(JSON.stringify({error: e?.message || 'OAuth callback failed'}), { diff --git a/app/image-tools/api/auth/github/start.ts b/app/image-tools/api/auth/github/start.ts new file mode 100644 index 0000000000..7ab605efd2 --- /dev/null +++ b/app/image-tools/api/auth/github/start.ts @@ -0,0 +1,32 @@ +export const config = {runtime: 'edge'}; + +import {createOAuthState, resolveOAuthReturnTo} from '../../../src/server/githubOAuthState'; + +export default async function (req: Request): Promise { + try { + if (req.method !== 'GET') return new Response('Method Not Allowed', {status: 405}); + + const requestUrl = new URL(req.url); + const nonce = requestUrl.searchParams.get('state') || ''; + const clientId = process.env.GITHUB_CLIENT_ID || process.env.VITE_GITHUB_CLIENT_ID; + const clientSecret = process.env.GITHUB_CLIENT_SECRET; + if (!clientId || !clientSecret) throw new Error('Missing GitHub OAuth env vars'); + + const returnTo = resolveOAuthReturnTo( + requestUrl.searchParams.get('returnTo'), + process.env.OAUTH_RETURN_ORIGINS || '' + ); + const state = await createOAuthState(nonce, returnTo, clientSecret); + const authorizeUrl = new URL('https://github.com/login/oauth/authorize'); + authorizeUrl.searchParams.set('client_id', clientId); + authorizeUrl.searchParams.set('state', state); + authorizeUrl.searchParams.set('scope', 'public_repo'); + authorizeUrl.searchParams.set('prompt', 'select_account'); + return Response.redirect(authorizeUrl.toString(), 302); + } catch (e: any) { + return new Response(JSON.stringify({error: e?.message || 'OAuth start failed'}), { + status: 400, + headers: {'Content-Type': 'application/json'} + }); + } +} diff --git a/app/image-tools/api/upload.ts b/app/image-tools/api/upload.ts index 05255c4212..b113e2007d 100644 --- a/app/image-tools/api/upload.ts +++ b/app/image-tools/api/upload.ts @@ -1,5 +1,3 @@ -export const config = {runtime: 'edge'}; - import {openPrWithFilesForkAware, getUserLogin} from './github'; const CANONICAL_OWNER = 'yearn'; @@ -165,7 +163,7 @@ async function readRequiredAssetFiles(form: FormData, suffix: string) { return {svgBytes, png32Bytes, png128Bytes}; } -export default async function (req: Request): Promise { +export async function handleUpload(req: Request): Promise { if (req.method !== 'POST') return new Response('Method Not Allowed', {status: 405}); try { const auth = req.headers.get('authorization') || ''; @@ -292,6 +290,9 @@ export default async function (req: Request): Promise { return jsonResponse({ok: true, prUrl}, 200); } catch (e: any) { + console.error('[upload] failed', e instanceof Error ? e.message : e); return jsonResponse({error: e?.message || 'Upload failed'}, e?.status || 500); } } + +export default {fetch: handleUpload}; diff --git a/app/image-tools/src/components/GithubSignIn.tsx b/app/image-tools/src/components/GithubSignIn.tsx index a93af3e5b3..11eabb21b0 100644 --- a/app/image-tools/src/components/GithubSignIn.tsx +++ b/app/image-tools/src/components/GithubSignIn.tsx @@ -24,7 +24,6 @@ type Props = { export const GithubSignIn: React.FC = ({token}) => { const [connecting, setConnecting] = useState(() => readAuthPending()); - const [configError, setConfigError] = useState(''); const [login, setLogin] = useState(null); useEffect(() => { @@ -56,16 +55,10 @@ export const GithubSignIn: React.FC = ({token}) => { const signIn = () => { const state = randomState(); - const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID; - if (!clientId) { - setConfigError('Missing GitHub client ID.'); - return; - } - setConfigError(''); storeAuthState(state); markAuthPending(); setConnecting(true); - window.location.href = buildAuthorizeUrl(clientId, state); + window.location.href = buildAuthorizeUrl(state); }; const signOut = () => { @@ -96,7 +89,6 @@ export const GithubSignIn: React.FC = ({token}) => { className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"> Sign in with GitHub - {configError &&

{configError}

} { - 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]); + const navigate = useNavigate(); + useEffect(() => { + const params = new URLSearchParams(window.location.hash.slice(1) || window.location.search); + const token = params.get('token'); + const state = params.get('state'); + const returnedState = readStoredState(); + clearAuthPending(); + const stateMatches = Boolean(state && returnedState && state === returnedState); + if (!stateMatches) { + console.warn('OAuth state mismatch.'); + } else if (token) { + storeAuthToken(token); + broadcastAuthChange(); + } + if (state) clearStoredState(); + navigate({to: '/'}); + }, [navigate]); - return

Signing you in…

; + return

Signing you in…

; }; export const GithubSuccessRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/auth/github/success', - component: GithubSuccessComponent, + getParentRoute: () => rootRoute, + path: '/auth/github/success', + component: GithubSuccessComponent }); diff --git a/app/image-tools/src/server/githubOAuthState.ts b/app/image-tools/src/server/githubOAuthState.ts new file mode 100644 index 0000000000..c48846b501 --- /dev/null +++ b/app/image-tools/src/server/githubOAuthState.ts @@ -0,0 +1,107 @@ +const CANONICAL_APP_ORIGIN = 'https://token-assets.yearn.fi'; +const TRUSTED_PREVIEW_HOSTS = new Set(['dev-vm.tail197cc7.ts.net']); +const STATE_TTL_MS = 10 * 60 * 1000; +const NONCE_PATTERN = /^[a-z0-9]{20,80}$/i; + +type OAuthStatePayload = { + nonce: string; + returnTo: string; + expiresAt: number; +}; + +export function resolveOAuthReturnTo(value: string | null, additionalOrigins = ''): string { + const candidate = new URL(value || CANONICAL_APP_ORIGIN); + const configuredOrigins = additionalOrigins + .split(',') + .map(origin => origin.trim()) + .filter(Boolean) + .map(origin => new URL(origin).origin); + const isCanonical = candidate.origin === CANONICAL_APP_ORIGIN; + const isConfigured = configuredOrigins.includes(candidate.origin); + const isTrustedPreview = + candidate.protocol === 'https:' && + TRUSTED_PREVIEW_HOSTS.has(candidate.hostname) && + !candidate.username && + !candidate.password; + + if (!isCanonical && !isConfigured && !isTrustedPreview) { + throw new Error('Untrusted OAuth return URL'); + } + + return candidate.origin; +} + +export async function createOAuthState( + nonce: string, + returnTo: string, + secret: string, + now = Date.now() +): Promise { + if (!NONCE_PATTERN.test(nonce)) throw new Error('Invalid OAuth state nonce'); + const payload: OAuthStatePayload = {nonce, returnTo, expiresAt: now + STATE_TTL_MS}; + const encodedPayload = encodeBase64Url(new TextEncoder().encode(JSON.stringify(payload))); + const signature = await sign(encodedPayload, secret); + return `${encodedPayload}.${encodeBase64Url(signature)}`; +} + +export async function readOAuthState(state: string, secret: string, now = Date.now()): Promise { + const [encodedPayload, encodedSignature, ...rest] = state.split('.'); + if (!encodedPayload || !encodedSignature || rest.length) throw new Error('Invalid OAuth state'); + + const key = await importSigningKey(secret); + const valid = await crypto.subtle.verify( + 'HMAC', + key, + decodeBase64Url(encodedSignature), + new TextEncoder().encode(encodedPayload) + ); + if (!valid) throw new Error('Invalid OAuth state signature'); + + const payload = JSON.parse(new TextDecoder().decode(decodeBase64Url(encodedPayload))) as OAuthStatePayload; + if (!NONCE_PATTERN.test(payload.nonce)) throw new Error('Invalid OAuth state nonce'); + if (!payload.returnTo || !Number.isFinite(payload.expiresAt) || payload.expiresAt < now) { + throw new Error('Expired OAuth state'); + } + return payload; +} + +export async function readOAuthStateWithLegacyProductionFallback( + state: string, + secret: string, + now = Date.now() +): Promise { + if (NONCE_PATTERN.test(state)) { + return {nonce: state, returnTo: CANONICAL_APP_ORIGIN, expiresAt: now}; + } + return readOAuthState(state, secret, now); +} + +async function sign(value: string, secret: string): Promise { + return crypto.subtle.sign('HMAC', await importSigningKey(secret), new TextEncoder().encode(value)); +} + +function importSigningKey(secret: string): Promise { + return crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(`token-assets-oauth-state:${secret}`), + {name: 'HMAC', hash: 'SHA-256'}, + false, + ['sign', 'verify'] + ); +} + +function encodeBase64Url(value: ArrayBuffer | Uint8Array): string { + const bytes = value instanceof Uint8Array ? value : new Uint8Array(value); + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function decodeBase64Url(value: string): ArrayBuffer { + const padded = value + .replace(/-/g, '+') + .replace(/_/g, '/') + .padEnd(Math.ceil(value.length / 4) * 4, '='); + const binary = atob(padded); + return Uint8Array.from(binary, char => char.charCodeAt(0)).buffer; +} diff --git a/app/image-tools/vercel.json b/app/image-tools/vercel.json index a17c448461..33dc7970ea 100644 --- a/app/image-tools/vercel.json +++ b/app/image-tools/vercel.json @@ -1,6 +1,9 @@ { - "$schema": "https://openapi.vercel.sh/vercel.json", - "rewrites": [ - { "source": "/(.*)", "destination": "/" } - ] + "$schema": "https://openapi.vercel.sh/vercel.json", + "functions": { + "api/upload.ts": { + "maxDuration": 60 + } + }, + "rewrites": [{"source": "/(.*)", "destination": "/"}] } diff --git a/app/image-tools/vite.config.ts b/app/image-tools/vite.config.ts index 467635e576..eb0f249a32 100644 --- a/app/image-tools/vite.config.ts +++ b/app/image-tools/vite.config.ts @@ -4,6 +4,10 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { - port: 5173 + port: 5173, + allowedHosts: ['dev-vm.tail197cc7.ts.net'] + }, + preview: { + allowedHosts: ['dev-vm.tail197cc7.ts.net'] } }); From b4d4502934f499e6655db09fea4fb2d98d8e585f Mon Sep 17 00:00:00 2001 From: rossgalloway <58150151+rossgalloway@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:31:12 +0000 Subject: [PATCH 2/2] init nextjs --- app/image-tools/.env.local.example | 34 +- app/image-tools/AGENTS.md | 58 +- app/image-tools/PRODUCT.md | 32 + app/image-tools/README.md | 79 ++- app/image-tools/api/erc20-name.ts | 94 --- app/image-tools/api/github.ts | 292 --------- app/image-tools/api/health.ts | 9 - app/image-tools/api/ping.ts | 9 - .../app/api/auth/github/callback/route.ts | 4 + .../app/api/auth/github/start/route.ts | 4 + app/image-tools/app/api/erc20-name/route.ts | 4 + app/image-tools/app/api/health/route.ts | 4 + app/image-tools/app/api/ping/route.ts | 4 + app/image-tools/app/api/upload/route.ts | 5 + .../app/auth/github/success/page-client.tsx | 41 ++ .../app/auth/github/success/page.tsx | 6 + app/image-tools/app/layout.tsx | 22 + app/image-tools/app/page-client.tsx | 8 + app/image-tools/app/page.tsx | 24 + app/image-tools/bun.lock | 600 +++--------------- app/image-tools/docs/improvement-review.md | 91 ++- app/image-tools/index.html | 12 - app/image-tools/next.config.ts | 11 + app/image-tools/package.json | 38 +- .../src/components/GithubSignIn.tsx | 20 +- app/image-tools/src/components/Header.tsx | 23 +- app/image-tools/src/index.css | 6 +- app/image-tools/src/lib/api.ts | 18 - app/image-tools/src/lib/chains.ts | 52 +- app/image-tools/src/lib/githubAuth.test.ts | 100 +++ app/image-tools/src/lib/githubAuth.ts | 59 +- app/image-tools/src/main.tsx | 16 - app/image-tools/src/router.tsx | 30 - .../src/routes/auth/github-success.tsx | 38 -- app/image-tools/src/routes/upload.tsx | 110 +--- app/image-tools/src/server/erc20Name.ts | 108 ++++ app/image-tools/src/server/github.ts | 373 +++++++++++ .../server/githubOAuthCallback.ts} | 12 +- .../server/githubOAuthStart.ts} | 8 +- .../src/server/githubOAuthState.test.ts | 40 ++ .../src/server/githubOAuthState.ts | 11 - app/image-tools/src/server/health.ts | 6 + app/image-tools/src/server/ping.ts | 6 + app/image-tools/src/server/upload.test.ts | 127 ++++ app/image-tools/{api => src/server}/upload.ts | 82 ++- app/image-tools/src/vite-env.d.ts | 2 - app/image-tools/tailwind.config.ts | 13 +- app/image-tools/tsconfig.json | 43 +- app/image-tools/vercel.json | 8 +- app/image-tools/vite.config.ts | 13 - 50 files changed, 1391 insertions(+), 1418 deletions(-) create mode 100644 app/image-tools/PRODUCT.md delete mode 100644 app/image-tools/api/erc20-name.ts delete mode 100644 app/image-tools/api/github.ts delete mode 100644 app/image-tools/api/health.ts delete mode 100644 app/image-tools/api/ping.ts create mode 100644 app/image-tools/app/api/auth/github/callback/route.ts create mode 100644 app/image-tools/app/api/auth/github/start/route.ts create mode 100644 app/image-tools/app/api/erc20-name/route.ts create mode 100644 app/image-tools/app/api/health/route.ts create mode 100644 app/image-tools/app/api/ping/route.ts create mode 100644 app/image-tools/app/api/upload/route.ts create mode 100644 app/image-tools/app/auth/github/success/page-client.tsx create mode 100644 app/image-tools/app/auth/github/success/page.tsx create mode 100644 app/image-tools/app/layout.tsx create mode 100644 app/image-tools/app/page-client.tsx create mode 100644 app/image-tools/app/page.tsx delete mode 100644 app/image-tools/index.html create mode 100644 app/image-tools/next.config.ts delete mode 100644 app/image-tools/src/lib/api.ts create mode 100644 app/image-tools/src/lib/githubAuth.test.ts delete mode 100644 app/image-tools/src/main.tsx delete mode 100644 app/image-tools/src/router.tsx delete mode 100644 app/image-tools/src/routes/auth/github-success.tsx create mode 100644 app/image-tools/src/server/erc20Name.ts create mode 100644 app/image-tools/src/server/github.ts rename app/image-tools/{api/auth/github/callback.ts => src/server/githubOAuthCallback.ts} (80%) rename app/image-tools/{api/auth/github/start.ts => src/server/githubOAuthStart.ts} (78%) create mode 100644 app/image-tools/src/server/githubOAuthState.test.ts create mode 100644 app/image-tools/src/server/health.ts create mode 100644 app/image-tools/src/server/ping.ts create mode 100644 app/image-tools/src/server/upload.test.ts rename app/image-tools/{api => src/server}/upload.ts (80%) delete mode 100644 app/image-tools/src/vite-env.d.ts delete mode 100644 app/image-tools/vite.config.ts diff --git a/app/image-tools/.env.local.example b/app/image-tools/.env.local.example index 334f8c25a0..408317d19e 100644 --- a/app/image-tools/.env.local.example +++ b/app/image-tools/.env.local.example @@ -1,22 +1,20 @@ -# 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_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 -VITE_RPC_URI_FOR_137=https://your_rpc_provider_for_chain_137 -VITE_RPC_URI_FOR_250=https://your_rpc_provider_for_chain_250 -VITE_RPC_URI_FOR_8453=https://your_rpc_provider_for_chain_8453 -VITE_RPC_URI_FOR_146=https://your_rpc_provider_for_chain_146 -VITE_RPC_URI_FOR_42161=https://your_rpc_provider_for_chain_42161 -VITE_RPC_URI_FOR_747474=https://your_rpc_provider_for_chain_747474 +# Optional browser-visible override. Defaults to the production OAuth broker. +NEXT_PUBLIC_GITHUB_OAUTH_BROKER_ORIGIN=https://token-assets.yearn.fi -# Server (Express) +# Server-only GitHub OAuth and upload configuration. 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 -REPO_OWNER=your-github-username-or-org +OAUTH_RETURN_ORIGINS=http://localhost:3000 +REPO_OWNER=yearn REPO_NAME=tokenAssets -# Optional: override API port -# PORT=5174 + +# Server-only RPC configuration. Legacy VITE_RPC_* names remain temporarily supported. +RPC_URI_FOR_1=https://your_rpc_provider_for_chain_1 +RPC_URI_FOR_10=https://your_rpc_provider_for_chain_10 +RPC_URI_FOR_100=https://your_rpc_provider_for_chain_100 +RPC_URI_FOR_137=https://your_rpc_provider_for_chain_137 +RPC_URI_FOR_250=https://your_rpc_provider_for_chain_250 +RPC_URI_FOR_8453=https://your_rpc_provider_for_chain_8453 +RPC_URI_FOR_146=https://your_rpc_provider_for_chain_146 +RPC_URI_FOR_42161=https://your_rpc_provider_for_chain_42161 +RPC_URI_FOR_747474=https://your_rpc_provider_for_chain_747474 diff --git a/app/image-tools/AGENTS.md b/app/image-tools/AGENTS.md index 8d06d80fd7..318a3884a3 100644 --- a/app/image-tools/AGENTS.md +++ b/app/image-tools/AGENTS.md @@ -2,47 +2,47 @@ ## Project Structure & Module Organization -- Assets: `tokens//
/` with `logo.svg`, `logo-32.png`, `logo-128.png`. -- Chains: `chains//` (numeric `chainId`). -- Image Upload App: `app/image-tools/`. -- Automation: `scripts/` (e.g., `ingestTokens.js`; inputs in `scripts/token-images-to-ingest/`). -- Root configs: `.editorconfig`, `.prettierrc`, `package.json`. +- Assets: `tokens//
/` with `logo.svg`, `logo-32.png`, `logo-128.png`. +- Chains: `chains//` (numeric `chainId`). +- Image Upload App: `app/image-tools/`. +- 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/*`). -- Build/preview: `bun build` then `bun preview`. -- Ingest assets: `node scripts/ingestTokens.js ./scripts/tokensToInjest.json` — copies prepared images into `tokens/`. +- Next.js dev: `bun dev` in `app/image-tools` (`http://127.0.0.1:3000`). +- The Next.js server serves both the UI and API routes under `/api/*`. +- Build/preview: `bun build` then `bun preview`. +- Ingest assets: `node scripts/ingestTokens.js ./scripts/tokensToInjest.json` — copies prepared images into `tokens/`. ## Coding Style & Naming Conventions -- Indentation: tabs, width 4 (`.editorconfig`). -- Prettier: single quotes, semicolons, 120‑column width. -- Token asset files: exactly `logo.svg`, `logo-32.png`, `logo-128.png`. -- Addresses: EVM lowercase. -- Directories: numeric `chainId`; addresses under the chain folder. +- Indentation: tabs, width 4 (`.editorconfig`). +- Prettier: single quotes, semicolons, 120‑column width. +- Token asset files: exactly `logo.svg`, `logo-32.png`, `logo-128.png`. +- Addresses: EVM lowercase. +- Directories: numeric `chainId`; addresses under the chain folder. ## Testing Guidelines -- No formal test suite. 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 PR URL. -- Ensure PNGs are exactly 32×32 and 128×128; keep SVGs optimized. +- Run `bun test`, `bun typecheck`, and `bun build`. Validate against the Next.js server: + - OAuth callback: `/api/auth/github/callback` returns to `/auth/github/success`. + - ERC-20 name lookup: POST `/api/erc20-name` (Node.js). + - Upload + PR: POST `/api/upload` (Node.js) and confirm PR URL. +- Ensure PNGs are exactly 32×32 and 128×128; keep SVGs optimized. ## Commit & Pull Request Guidelines -- Commits: conventional prefixes preferred (e.g., `chore:`, `fix:`). -- PRs must include: - - Change summary and rationale. - - Exact path(s) added/changed (e.g., `tokens/1/0xabc.../`). - - Sample API URL(s) and, if helpful, screenshots of rendered PNGs. - - Linked issues or context. +- Commits: conventional prefixes preferred (e.g., `chore:`, `fix:`). +- PRs must include: + - Change summary and rationale. + - Exact path(s) added/changed (e.g., `tokens/1/0xabc.../`). + - Sample API URL(s) and, if helpful, screenshots of rendered PNGs. + - Linked issues or context. ## Security & Configuration Tips -- Do not commit secrets or binaries outside `tokens/` build outputs. -- Optimize SVGs (small, simple paths). -- Ensure PNGs are precisely sized (32×32, 128×128). -- Unless explicitly requested, avoid editing anything under `/_config`. +- Do not commit secrets or binaries outside `tokens/` build outputs. +- Optimize SVGs (small, simple paths). +- Ensure PNGs are precisely sized (32×32, 128×128). +- Unless explicitly requested, avoid editing anything under `/_config`. diff --git a/app/image-tools/PRODUCT.md b/app/image-tools/PRODUCT.md new file mode 100644 index 0000000000..71d4d7a53a --- /dev/null +++ b/app/image-tools/PRODUCT.md @@ -0,0 +1,32 @@ +# Product + +## Register + +product + +## Users + +Yearn contributors using an internal tool to prepare token and chain assets and open consistent repository pull requests. + +## Product Purpose + +Make asset submission quick, reliable, and easy to review while enforcing the repository's file and security requirements. + +## Brand Personality + +Simple, clean, dependable. + +## Anti-references + +Avoid marketing-heavy crypto styling, decorative effects, visual noise, and interactions that obscure the upload workflow. + +## Design Principles + +- Keep the primary upload task obvious. +- Make repository and authentication context easy to verify. +- Prefer concise controls and actionable feedback. +- Preserve contributor confidence through predictable behavior. + +## Accessibility & Inclusion + +Use WCAG AA as the baseline, including keyboard access, visible focus states, meaningful labels, and sufficient contrast. diff --git a/app/image-tools/README.md b/app/image-tools/README.md index 23849e0955..824beac722 100644 --- a/app/image-tools/README.md +++ b/app/image-tools/README.md @@ -1,43 +1,54 @@ # Image Tools App -A lightweight SPA + Vercel Functions app for uploading token/chain assets and opening GitHub PRs into this repository. - -## Environment Variables (Dev/Prod) - -- Client (exposed to browser) - - `VITE_GITHUB_OAUTH_BROKER_ORIGIN` — optional; defaults to `https://token-assets.yearn.fi`, whose registered - callback brokers sign-in back to approved preview origins. - - `VITE_API_BASE_URL` — optional; default same-origin. Set only if the API lives on another origin. - - `VITE_RPC_URI_FOR_` — optional RPC URLs used by `/api/erc20-name`. -- Server (Vercel Functions) - - `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — GitHub OAuth App credentials used by the production broker. - - `OAUTH_RETURN_ORIGINS` — optional comma-separated list of additional exact origins allowed after GitHub OAuth. - The canonical production origin and the private `dev-vm.tail197cc7.ts.net` preview host are already trusted. - - `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. -- GitHub OAuth App callback must be configured to the deployed API callback URL: - `https:///api/auth/github/callback`. The client intentionally does not send a `redirect_uri`; GitHub - uses the callback URL registered on the OAuth App. The signed OAuth state then safely returns the user to the - production or approved preview origin that started sign-in. +A Next.js app for uploading token and chain assets and opening pull requests against this repository. + +## Environment variables + +### Browser + +- `NEXT_PUBLIC_GITHUB_OAUTH_BROKER_ORIGIN` — optional; defaults to `https://token-assets.yearn.fi`. The production + broker uses its registered GitHub callback and safely returns users to approved preview origins. + +### Server + +- `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — GitHub OAuth App credentials used by the production broker. +- `OAUTH_RETURN_ORIGINS` — optional comma-separated list of additional exact origins allowed after GitHub OAuth. + The canonical production origin and private `dev-vm.tail197cc7.ts.net` preview host are already trusted. +- `REPO_OWNER` and `REPO_NAME` — default to `yearn/tokenAssets`. +- `ALLOW_REPO_OVERRIDE` — set to `true` only when intentionally targeting another repository from a fork deployment. +- `RPC_URI_FOR_` or `RPC_` — optional RPC URLs used by `/api/erc20-name`. Legacy `VITE_RPC_*` + names are temporarily accepted during deployment migration. + +The GitHub OAuth App callback must be `https://token-assets.yearn.fi/api/auth/github/callback`. Preview sign-in starts +at the production `/api/auth/github/start` broker, which signs the requested app origin into OAuth state. GitHub then +returns to the registered production callback, and the broker returns the token in the URL fragment of the approved +preview or production `/auth/github/success` route. + +### OAuth deployment transition + +The pre-migration production Vite client starts GitHub OAuth with an unsigned nonce. The Next.js callback intentionally +does not accept that legacy state. An OAuth attempt started in an old or already-open Vite tab before the Next.js +deployment will fail after cutover; the user must refresh and start sign-in again. Deploy the Next.js frontend, OAuth +start route, and callback together so all newly started attempts use signed, expiring state. ## Commands -- `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 dev` — Next.js development server at `http://127.0.0.1:3000`. +- `bun build` — production Next.js build. +- `bun preview` — serve the production build at `http://127.0.0.1:3000`. +- `bun typecheck` / `bun lint` — TypeScript validation. +- `bun test` — focused Bun tests for OAuth and upload behavior. -## App Flow (What Calls What) +Node.js 20.9 or newer is required. Vercel should use `app/image-tools` as the project root and the Next.js framework +preset. -1. Open the site — SPA loads; no API calls by default. -2. Sign in with GitHub — Browser starts at the production OAuth broker, which signs the requested app origin into the OAuth state before redirecting to GitHub. GitHub always returns to the registered production `/api/auth/github/callback` (Edge), which verifies the state, exchanges the code, and safely returns to `/auth/github/success` on the original production or preview origin. -3. Enter chain/address — Client may call `POST /api/erc20-name` (Edge) to resolve ERC‑20 name. -4. Drop SVG — Client generates PNG previews (32×32, 128×128) via Canvas. -5. Submit PR — Client posts multipart form to `POST /api/upload` (Node.js) with `svg`, `png32`, and `png128`. The function validates sizes and opens a PR via GitHub API. GitHub's tree API creates missing token or chain directories from the submitted file paths. +## App flow -## Notes +1. The App Router serves the upload form. +2. GitHub sign-in uses the production OAuth broker and returns through `/auth/github/success`. +3. The client calls `POST /api/erc20-name` to resolve ERC-20 names through a server-only RPC. +4. Dropping an SVG generates 32×32 and 128×128 PNG previews in the browser. +5. `POST /api/upload` validates the multipart files and opens a GitHub pull request. -- PNGs are generated client‑side and validated on the server. -- Keep SVGs simple/optimized; ensure PNGs are exactly 32×32 and 128×128. +Git tree entries create missing directories implicitly. A chain `999` upload submits +`chains/999/{logo.svg,logo-32.png,logo-128.png}` even when `chains/999` does not exist yet. 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/app/api/auth/github/callback/route.ts b/app/image-tools/app/api/auth/github/callback/route.ts new file mode 100644 index 0000000000..25d9c39987 --- /dev/null +++ b/app/image-tools/app/api/auth/github/callback/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export {handleGithubOAuthCallback as GET} from '@/src/server/githubOAuthCallback'; diff --git a/app/image-tools/app/api/auth/github/start/route.ts b/app/image-tools/app/api/auth/github/start/route.ts new file mode 100644 index 0000000000..9af391c39b --- /dev/null +++ b/app/image-tools/app/api/auth/github/start/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export {handleGithubOAuthStart as GET} from '@/src/server/githubOAuthStart'; diff --git a/app/image-tools/app/api/erc20-name/route.ts b/app/image-tools/app/api/erc20-name/route.ts new file mode 100644 index 0000000000..61f0d024a8 --- /dev/null +++ b/app/image-tools/app/api/erc20-name/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export {handleErc20Name as POST} from '@/src/server/erc20Name'; diff --git a/app/image-tools/app/api/health/route.ts b/app/image-tools/app/api/health/route.ts new file mode 100644 index 0000000000..ff798eb99c --- /dev/null +++ b/app/image-tools/app/api/health/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export {handleHealth as GET} from '@/src/server/health'; diff --git a/app/image-tools/app/api/ping/route.ts b/app/image-tools/app/api/ping/route.ts new file mode 100644 index 0000000000..23eea6ab89 --- /dev/null +++ b/app/image-tools/app/api/ping/route.ts @@ -0,0 +1,4 @@ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export {handlePing as GET} from '@/src/server/ping'; diff --git a/app/image-tools/app/api/upload/route.ts b/app/image-tools/app/api/upload/route.ts new file mode 100644 index 0000000000..d9ac59644a --- /dev/null +++ b/app/image-tools/app/api/upload/route.ts @@ -0,0 +1,5 @@ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + +export {handleUpload as POST} from '@/src/server/upload'; diff --git a/app/image-tools/app/auth/github/success/page-client.tsx b/app/image-tools/app/auth/github/success/page-client.tsx new file mode 100644 index 0000000000..e678ffabd5 --- /dev/null +++ b/app/image-tools/app/auth/github/success/page-client.tsx @@ -0,0 +1,41 @@ +'use client'; + +import Link from 'next/link'; +import {useRouter} from 'next/navigation'; +import type {ReactElement} from 'react'; +import {useEffect, useRef, useState} from 'react'; +import {completeGithubOAuthSignIn} from '../../../../src/lib/githubAuth'; + +export default function GithubSuccessPageClient(): ReactElement { + const router = useRouter(); + const completed = useRef(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (completed.current) return; + completed.current = true; + + const result = completeGithubOAuthSignIn(window.location.hash); + window.history.replaceState(null, '', window.location.pathname); + if (result.ok) router.replace('/'); + else setError(result.error); + }, [router]); + + if (error) { + return ( +
+

GitHub sign-in failed

+

{error}

+ + Return to the uploader + +
+ ); + } + + return

Signing you in…

; +} diff --git a/app/image-tools/app/auth/github/success/page.tsx b/app/image-tools/app/auth/github/success/page.tsx new file mode 100644 index 0000000000..37d4078cd6 --- /dev/null +++ b/app/image-tools/app/auth/github/success/page.tsx @@ -0,0 +1,6 @@ +import type {ReactElement} from 'react'; +import GithubSuccessPageClient from './page-client'; + +export default function GithubSuccessPage(): ReactElement { + return ; +} diff --git a/app/image-tools/app/layout.tsx b/app/image-tools/app/layout.tsx new file mode 100644 index 0000000000..154ecacc4e --- /dev/null +++ b/app/image-tools/app/layout.tsx @@ -0,0 +1,22 @@ +import type {Metadata} from 'next'; +import type {ReactNode} from 'react'; +import {Header} from '../src/components/Header'; +import '../src/index.css'; + +export const metadata: Metadata = { + title: 'Image Tools', + description: 'Upload token and chain assets to the Yearn token assets repository.' +}; + +export default function RootLayout({children}: Readonly<{children: ReactNode}>) { + return ( + + +
+
+
{children}
+
+ + + ); +} diff --git a/app/image-tools/app/page-client.tsx b/app/image-tools/app/page-client.tsx new file mode 100644 index 0000000000..23bea3e5f5 --- /dev/null +++ b/app/image-tools/app/page-client.tsx @@ -0,0 +1,8 @@ +'use client'; + +import type {ReactElement} from 'react'; +import {UploadComponent, type UploadUrlParams} from '../src/routes/upload'; + +export default function UploadPageClient({initialParams}: {initialParams: UploadUrlParams}): ReactElement { + return ; +} diff --git a/app/image-tools/app/page.tsx b/app/image-tools/app/page.tsx new file mode 100644 index 0000000000..0eef11b49a --- /dev/null +++ b/app/image-tools/app/page.tsx @@ -0,0 +1,24 @@ +import type {ReactElement} from 'react'; +import UploadPageClient from './page-client'; + +type SearchParams = Record; + +function readFirstSearchValue(searchParams: SearchParams, keys: string[]): string | undefined { + for (const key of keys) { + const rawValue = searchParams[key]; + const value = (Array.isArray(rawValue) ? rawValue[0] : rawValue)?.trim(); + if (value) return value; + } + return undefined; +} + +export default async function UploadPage({searchParams}: {searchParams: Promise}): Promise { + const resolvedSearchParams = await searchParams; + const modeParam = readFirstSearchValue(resolvedSearchParams, ['mode', 'type', 'target']); + const chainId = readFirstSearchValue(resolvedSearchParams, ['chain', 'chainId']); + const address = readFirstSearchValue(resolvedSearchParams, ['address', 'token']); + const name = readFirstSearchValue(resolvedSearchParams, ['name']); + const mode = modeParam === 'chain' ? 'chain' : modeParam === 'token' || address ? 'token' : undefined; + + return ; +} diff --git a/app/image-tools/bun.lock b/app/image-tools/bun.lock index ac2d261425..5517ab3942 100644 --- a/app/image-tools/bun.lock +++ b/app/image-tools/bun.lock @@ -1,193 +1,121 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "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", - "@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", + "@headlessui/react": "2.2.10", + "next": "16.2.6", + "react": "19.2.6", + "react-dom": "19.2.6", }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/multer": "^1.4.11", - "@vitejs/plugin-react": "^4.2.0", + "@types/bun": "1.3.14", + "@types/node": "25.3.0", + "@types/react": "19.2.15", + "@types/react-dom": "19.2.3", "autoprefixer": "^10.4.19", "postcss": "^8.4.41", "tailwindcss": "^3.4.9", - "tsx": "^4.7.0", - "typescript": "^5.4.0", - "vite": "^5.2.0", + "typescript": "5.9.3", }, }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@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=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], - - "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - - "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - - "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - - "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], - - "@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=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="], + "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="], + "@headlessui/react": ["@headlessui/react@2.2.10", "", { "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-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "@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-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "@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-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "@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-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "@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-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - "@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-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - "@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-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@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=="], + "@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=="], - "@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=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "@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=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@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=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@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=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], - "@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=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -211,113 +139,19 @@ "@react-types/shared": ["@react-types/shared@3.32.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="], - - "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - - "@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/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-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-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=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@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/store": ["@tanstack/store@0.7.4", "", {}, "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@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/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], - "@types/multer": ["@types/multer@1.4.13", "", { "dependencies": { "@types/express": "*" } }, "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw=="], - - "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], - - "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], - - "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], - - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], - - "@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=="], - - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], @@ -327,19 +161,15 @@ "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=="], - "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=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], - "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=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -347,15 +177,7 @@ "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=="], - - "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=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], @@ -363,134 +185,56 @@ "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=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "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=="], - "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=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - - "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "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=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "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=="], - - "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=="], - "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=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "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=="], - "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=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "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=="], - - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -503,65 +247,31 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "isbot": ["isbot@5.1.30", "", {}, "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "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=="], - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - "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=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "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=="], - "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=="], + "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], "node-releases": ["node-releases@2.0.20", "", {}, "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA=="], @@ -573,22 +283,14 @@ "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=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "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=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -611,94 +313,46 @@ "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=="], - - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], - - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], - "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": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "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=="], - - "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], "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=="], "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=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "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=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "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=="], - - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "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=="], - - "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=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "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=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "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=="], - "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=="], - - "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], - "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=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -711,29 +365,15 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - - "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], - "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=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "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=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "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=="], @@ -741,112 +381,46 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "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=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "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=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@react-aria/focus/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "@react-aria/interactions/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@babel/core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "@react-aria/ssr/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@react-aria/utils/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@react-stately/flags/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "@react-stately/utils/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "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=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "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=="], - "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/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=="], - - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - "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=="], diff --git a/app/image-tools/docs/improvement-review.md b/app/image-tools/docs/improvement-review.md index d67e34884f..cce55ef34d 100644 --- a/app/image-tools/docs/improvement-review.md +++ b/app/image-tools/docs/improvement-review.md @@ -2,126 +2,123 @@ _Last updated: 2025-09-17_ +> Historical review: file paths and tooling references below describe the pre-Next.js Vite implementation. + ## Key Priorities -- Break the upload workflow into smaller, testable modules and shared utilities to reduce the 1.1k-line `UploadComponent` and remove duplicated preview/state logic (`src/routes/upload.tsx:1-1139`). -- Harden the upload API surface with server-side address validation, reusable file validation helpers, and consistent metadata handling to prevent malformed submissions (`api/upload.ts:80-200`). -- Tighten OAuth and GitHub integration flows by using crypto-safe state generation, caching the user profile, and centralising auth state updates to remove repeated storage/event wiring (`src/components/GithubSignIn.tsx:17-124`, `src/components/Header.tsx:6-27`). +- Break the upload workflow into smaller, testable modules and shared utilities to reduce the 1.1k-line `UploadComponent` and remove duplicated preview/state logic (`src/routes/upload.tsx:1-1139`). +- Harden the upload API surface with server-side address validation, reusable file validation helpers, and consistent metadata handling to prevent malformed submissions (`api/upload.ts:80-200`). +- Tighten OAuth and GitHub integration flows by using crypto-safe state generation, caching the user profile, and centralising auth state updates to remove repeated storage/event wiring (`src/components/GithubSignIn.tsx:17-124`, `src/components/Header.tsx:6-27`). ## Frontend SPA (`src/`) ### Upload workflow (`src/routes/upload.tsx`) -- Issue: The entire form, preview generation, PR modal, and API orchestration sit inside a single 1,100+ line component (`src/routes/upload.tsx:1-1139`), making it hard to reason about and test. The first token shares a separate code path from additional tokens, causing duplicated event handlers and preview renderers. +- Issue: The entire form, preview generation, PR modal, and API orchestration sit inside a single 1,100+ line component (`src/routes/upload.tsx:1-1139`), making it hard to reason about and test. The first token shares a separate code path from additional tokens, causing duplicated event handlers and preview renderers. -- Recommendation: Extract a `useUploadForm` hook to own the shared state (token arrays, chain assets, submission logic) and split the UI into `TokenAssetCard`, `ChainAssetCard`, `PreviewPanel`, and `ReviewDialog` components. This keeps render logic declarative and enables targeted unit tests. +- Recommendation: Extract a `useUploadForm` hook to own the shared state (token arrays, chain assets, submission logic) and split the UI into `TokenAssetCard`, `ChainAssetCard`, `PreviewPanel`, and `ReviewDialog` components. This keeps render logic declarative and enables targeted unit tests. --- -- Issue: PNG preview generation logic is duplicated for chain and token flows (`src/routes/upload.tsx:158-223`) and relies on a stringified dependency array to watch file changes (`src/routes/upload.tsx:225-233`). It also re-instantiates `TextDecoder` per call (`src/routes/upload.tsx:129-131`) and does not revoke object URLs created in `onTokenFileChange` (`src/routes/upload.tsx:143-155`) after replacement. +- Issue: PNG preview generation logic is duplicated for chain and token flows (`src/routes/upload.tsx:158-223`) and relies on a stringified dependency array to watch file changes (`src/routes/upload.tsx:225-233`). It also re-instantiates `TextDecoder` per call (`src/routes/upload.tsx:129-131`) and does not revoke object URLs created in `onTokenFileChange` (`src/routes/upload.tsx:143-155`) after replacement. -- Recommendation: Move preview generation to a shared utility like `lib/imagePreview.ts` that returns both PNG sizes and handles cleanup with `try/finally` and `URL.revokeObjectURL`. Drive effects off explicit `File` references or IDs instead of `JSON.stringify` to remove unnecessary recalculations. +- Recommendation: Move preview generation to a shared utility like `lib/imagePreview.ts` that returns both PNG sizes and handles cleanup with `try/finally` and `URL.revokeObjectURL`. Drive effects off explicit `File` references or IDs instead of `JSON.stringify` to remove unnecessary recalculations. - --- + *** -- Issue: Both the blur handlers that resolve ERC-20 names (`src/routes/upload.tsx:400-454`, `src/routes/upload.tsx:770-838`) duplicate async logic and mix UI effects with data fetching, while `fetchErc20Name` reimplements the ABI decoding already present in `api/erc20-name.ts`. +- Issue: Both the blur handlers that resolve ERC-20 names (`src/routes/upload.tsx:400-454`, `src/routes/upload.tsx:770-838`) duplicate async logic and mix UI effects with data fetching, while `fetchErc20Name` reimplements the ABI decoding already present in `api/erc20-name.ts`. -- Recommendation: Extract a shared helper for name resolution that lives in `src/lib/erc20.ts` and reuse it in both the component and the API. Wrap the async call in a cancellable hook using TanStack Query so stale responses do not update state. +- Recommendation: Extract a shared helper for name resolution that lives in `src/lib/erc20.ts` and reuse it in both the component and the API. Wrap the async call in a cancellable hook using TanStack Query so stale responses do not update state. --- -- Issue: `buildFormData` (`src/routes/upload.tsx:262-308`) awaits PNG conversions sequentially and appends addresses under a shared key, which can desynchronise with the indexed `svg_*` fields if users leave gaps. The metadata builder later relies on array indexes staying in sync (`src/routes/upload.tsx:237-245`). - -- Recommendation: Build an explicit array of `TokenSubmission` objects before serialisation, validate each entry, and use `Promise.all` to convert missing PNGs concurrently. Send structured JSON alongside the files (e.g., `body.append('tokens', JSON.stringify(tokens))`) so server-side parsing is deterministic. +- Issue: `buildFormData` (`src/routes/upload.tsx:262-308`) awaits PNG conversions sequentially and appends addresses under a shared key, which can desynchronise with the indexed `svg_*` fields if users leave gaps. The metadata builder later relies on array indexes staying in sync (`src/routes/upload.tsx:237-245`). +- Recommendation: Build an explicit array of `TokenSubmission` objects before serialisation, validate each entry, and use `Promise.all` to convert missing PNGs concurrently. Send structured JSON alongside the files (e.g., `body.append('tokens', JSON.stringify(tokens))`) so server-side parsing is deterministic. --- -- Issue: Toggle actions and manual DOM manipulations (`document.createElement('input')` on `src/routes/upload.tsx:604-618`) bypass React’s declarative model and complicate testing/accessibility. +- Issue: Toggle actions and manual DOM manipulations (`document.createElement('input')` on `src/routes/upload.tsx:604-618`) bypass React’s declarative model and complicate testing/accessibility. -- Recommendation: Replace manual input creation with hidden file inputs controlled via refs, and rely on controlled `Switch` components with accessible labelling (`aria-checked`, `aria-labelledby`). +- Recommendation: Replace manual input creation with hidden file inputs controlled via refs, and rely on controlled `Switch` components with accessible labelling (`aria-checked`, `aria-labelledby`). ### Auth components (`src/components/GithubSignIn.tsx`, `src/components/Header.tsx`) -- Issue: OAuth state randomness relies on `Math.random` (`src/components/GithubSignIn.tsx:17-22`) and repeats storage synchronisation logic already handled in `Header` (`src/components/Header.tsx:6-27`). +- Issue: OAuth state randomness relies on `Math.random` (`src/components/GithubSignIn.tsx:17-22`) and repeats storage synchronisation logic already handled in `Header` (`src/components/Header.tsx:6-27`). -- Recommendation: Use `crypto.getRandomValues` (with a fallback for older browsers) for the state string and centralise auth event subscription in a dedicated `useGithubAuth` hook consumed by both `Header` and `GithubSignIn`. +- Recommendation: Use `crypto.getRandomValues` (with a fallback for older browsers) for the state string and centralise auth event subscription in a dedicated `useGithubAuth` hook consumed by both `Header` and `GithubSignIn`. --- -- Issue: `GithubSignIn` fetches the user profile directly from GitHub on every token change (`src/components/GithubSignIn.tsx:57-70`) without caching or cancellation, and errors fall back to `alert` usage (`src/components/GithubSignIn.tsx:77-90`). +- Issue: `GithubSignIn` fetches the user profile directly from GitHub on every token change (`src/components/GithubSignIn.tsx:57-70`) without caching or cancellation, and errors fall back to `alert` usage (`src/components/GithubSignIn.tsx:77-90`). -- Recommendation: Provide a lightweight client wrapper over the repo’s API routes that proxy GitHub, cache the profile with TanStack Query (already bundled but unused), and surface non-blocking UI feedback instead of `alert`. +- Recommendation: Provide a lightweight client wrapper over the repo’s API routes that proxy GitHub, cache the profile with TanStack Query (already bundled but unused), and surface non-blocking UI feedback instead of `alert`. --- -- Issue: `Header` re-mounts `GithubSignIn` using the `key` prop (`src/components/Header.tsx:27`), causing modal state to reset whenever auth changes. - -- Recommendation: Pass the token down as props or via context so child components control their own state transitions without re-mounting. +- Issue: `Header` re-mounts `GithubSignIn` using the `key` prop (`src/components/Header.tsx:27`), causing modal state to reset whenever auth changes. +- Recommendation: Pass the token down as props or via context so child components control their own state transitions without re-mounting. ## API Layer (`api/`) ### Upload endpoint (`api/upload.ts`) -- Issue: The endpoint expects matching `address`, `svg_*`, and `chainId_*` inputs but filters out empty addresses before iterating (`api/upload.ts:94-118`), which can desynchronise indexes and skip related files. No server-side address validation exists. +- Issue: The endpoint expects matching `address`, `svg_*`, and `chainId_*` inputs but filters out empty addresses before iterating (`api/upload.ts:94-118`), which can desynchronise indexes and skip related files. No server-side address validation exists. -- Recommendation: Parse submissions based on `svg_*` indices, validate each entry (including an `isEvmAddress` check), and return structured error messages per asset. Reuse the same `TokenItem` schema the client uses to guarantee alignment. +- Recommendation: Parse submissions based on `svg_*` indices, validate each entry (including an `isEvmAddress` check), and return structured error messages per asset. Reuse the same `TokenItem` schema the client uses to guarantee alignment. --- -- Issue: PNG validation and base64 encoding logic is duplicated for both token and chain flows (`api/upload.ts:120-165`, `api/upload.ts:166-200`). +- Issue: PNG validation and base64 encoding logic is duplicated for both token and chain flows (`api/upload.ts:120-165`, `api/upload.ts:166-200`). -- Recommendation: Extract reusable helpers (`readPng`, `assertPngDimensions`, `toRepoPath`) so both branches share the same validation surface and tests can cover them. +- Recommendation: Extract reusable helpers (`readPng`, `assertPngDimensions`, `toRepoPath`) so both branches share the same validation surface and tests can cover them. --- -- Issue: The GitHub PR creation flow repeats blob/tree logic for direct and forked uploads (`api/github.ts:140-207`), leading to duplicated fetch sequences and no retry strategy. +- Issue: The GitHub PR creation flow repeats blob/tree logic for direct and forked uploads (`api/github.ts:140-207`), leading to duplicated fetch sequences and no retry strategy. -- Recommendation: Build a shared helper that prepares the commit tree once, then injects owner/repo context. Introduce clearer error mapping (403 vs 409) and surface actionable feedback to the client (e.g., 409 → “sync fork”). +- Recommendation: Build a shared helper that prepares the commit tree once, then injects owner/repo context. Introduce clearer error mapping (403 vs 409) and surface actionable feedback to the client (e.g., 409 → “sync fork”). --- -- Issue: Environment handling (`resolveTargetRepo`) depends on multiple env combinations but lacks diagnostics when misconfigured (`api/upload.ts:9-34`). +- Issue: Environment handling (`resolveTargetRepo`) depends on multiple env combinations but lacks diagnostics when misconfigured (`api/upload.ts:9-34`). -- Recommendation: Log the chosen target repo and return descriptive errors when overrides are ignored so deployments can be verified quickly. +- Recommendation: Log the chosen target repo and return descriptive errors when overrides are ignored so deployments can be verified quickly. ### ERC-20 name endpoint (`api/erc20-name.ts`) -- Issue: Duplicate ABI decoding logic with the client and no caching or rate limiting (`api/erc20-name.ts:14-55`). - -- Recommendation: Move the decoder to a shared utility, implement a simple in-memory cache for repeated lookups within the same request lifecycle, and allow RPC URL overrides via configuration tested at startup. +- Issue: Duplicate ABI decoding logic with the client and no caching or rate limiting (`api/erc20-name.ts:14-55`). +- Recommendation: Move the decoder to a shared utility, implement a simple in-memory cache for repeated lookups within the same request lifecycle, and allow RPC URL overrides via configuration tested at startup. ## Shared utilities & configuration -- Issue: `API_BASE_URL` falls back to `'http://localhost'` for non-browser contexts (`src/lib/api.ts:8-14`), which is incorrect when deployed to Vercel edge. - -- Recommendation: Default to `'/'` and require explicit overrides, or accept the request origin via dependency injection. +- Issue: `API_BASE_URL` falls back to `'http://localhost'` for non-browser contexts (`src/lib/api.ts:8-14`), which is incorrect when deployed to Vercel edge. +- Recommendation: Default to `'/'` and require explicit overrides, or accept the request origin via dependency injection. --- -- Issue: ABI decoding and EVM helpers are split between client and server (`src/routes/upload.tsx:112-131`, `api/erc20-name.ts:14-33`). - -- Recommendation: Create `shared/evm.ts` (or reuse existing libs) to consolidate address validation, RPC configuration, and ABI decoding logic. +- Issue: ABI decoding and EVM helpers are split between client and server (`src/routes/upload.tsx:112-131`, `api/erc20-name.ts:14-33`). +- Recommendation: Create `shared/evm.ts` (or reuse existing libs) to consolidate address validation, RPC configuration, and ABI decoding logic. --- -- Issue: `@tanstack/react-query` is bundled but unused beyond the provider (`src/main.tsx:4-12`), increasing bundle size unnecessarily. +- Issue: `@tanstack/react-query` is bundled but unused beyond the provider (`src/main.tsx:4-12`), increasing bundle size unnecessarily. -- Recommendation: Either leverage it for name lookup/profile caching or remove the dependency and provider. +- Recommendation: Either leverage it for name lookup/profile caching or remove the dependency and provider. ## Developer Experience -- Issue: There is no linting beyond TypeScript’s structural checks. +- Issue: There is no linting beyond TypeScript’s structural checks. -- Recommendation: Add an ESLint setup with `@typescript-eslint` and `eslint-plugin-react` to enforce hooks rules, dependency arrays, and accessibility best practices. +- Recommendation: Add an ESLint setup with `@typescript-eslint` and `eslint-plugin-react` to enforce hooks rules, dependency arrays, and accessibility best practices. --- -- Issue: PNG/SVG utilities and GitHub integration lack automated tests. +- Issue: PNG/SVG utilities and GitHub integration lack automated tests. -- Recommendation: Introduce `vitest` (already compatible with Vite) and cover `pngDimensions`, repo path builders, and auth storage utilities. Run the suite via CI to catch regressions. +- Recommendation: Introduce `vitest` (already compatible with Vite) and cover `pngDimensions`, repo path builders, and auth storage utilities. Run the suite via CI to catch regressions. --- -- Issue: Build scripts rely on Bun, but contributors may still use Node. +- Issue: Build scripts rely on Bun, but contributors may still use Node. -- Recommendation: Document equivalent `npm` scripts or add package.json aliases so contributors without Bun can run builds. +- Recommendation: Document equivalent `npm` scripts or add package.json aliases so contributors without Bun can run builds. diff --git a/app/image-tools/index.html b/app/image-tools/index.html deleted file mode 100644 index b35e75f123..0000000000 --- a/app/image-tools/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Image Tools - - -
- - - diff --git a/app/image-tools/next.config.ts b/app/image-tools/next.config.ts new file mode 100644 index 0000000000..e3bfb2b85d --- /dev/null +++ b/app/image-tools/next.config.ts @@ -0,0 +1,11 @@ +import type {NextConfig} from 'next'; + +const nextConfig: NextConfig = { + reactStrictMode: true, + poweredByHeader: false, + turbopack: { + root: process.cwd() + } +}; + +export default nextConfig; diff --git a/app/image-tools/package.json b/app/image-tools/package.json index f8cdade2db..2ae9a1749d 100644 --- a/app/image-tools/package.json +++ b/app/image-tools/package.json @@ -3,29 +3,31 @@ "private": true, "version": "0.1.0", "type": "module", + "engines": { + "node": ">=20.9.0" + }, "scripts": { - "dev": "vite", - "typecheck": "tsc -noEmit", - "lint": "tsc -noEmit", - "build": "vite build", - "preview": "vite preview" + "dev": "next dev --hostname 127.0.0.1 --port 3000", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "build": "next build", + "preview": "next start --hostname 127.0.0.1 --port 3000", + "test": "bun test" }, "dependencies": { - "@headlessui/react": "^2.1.10", - "@tanstack/react-query": "^5.51.3", - "@tanstack/react-router": "^1.47.0", - "@types/react": "^19.1.12", - "@types/react-dom": "^19.1.9", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@headlessui/react": "2.2.10", + "next": "16.2.6", + "react": "19.2.6", + "react-dom": "19.2.6" }, "devDependencies": { - "typescript": "^5.4.0", - "vite": "^5.2.0", - "@vitejs/plugin-react": "^4.2.0", - "tsx": "^4.7.0", - "tailwindcss": "^3.4.9", + "@types/bun": "1.3.14", + "@types/node": "25.3.0", + "@types/react": "19.2.15", + "@types/react-dom": "19.2.3", + "autoprefixer": "^10.4.19", "postcss": "^8.4.41", - "autoprefixer": "^10.4.19" + "tailwindcss": "^3.4.9", + "typescript": "5.9.3" } } diff --git a/app/image-tools/src/components/GithubSignIn.tsx b/app/image-tools/src/components/GithubSignIn.tsx index 11eabb21b0..8252aef1b0 100644 --- a/app/image-tools/src/components/GithubSignIn.tsx +++ b/app/image-tools/src/components/GithubSignIn.tsx @@ -3,21 +3,15 @@ import {Dialog, Transition} from '@headlessui/react'; import { broadcastAuthChange, - storeAuthState, - clearStoredAuth, buildAuthorizeUrl, - markAuthPending, clearAuthPending, - readAuthPending + clearStoredAuth, + createGithubOAuthNonce, + markAuthPending, + readAuthPending, + storeAuthState } 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; -} - type Props = { token: string | null; }; @@ -54,7 +48,7 @@ export const GithubSignIn: React.FC = ({token}) => { }, [token]); const signIn = () => { - const state = randomState(); + const state = createGithubOAuthNonce(); storeAuthState(state); markAuthPending(); setConnecting(true); @@ -86,7 +80,7 @@ export const GithubSignIn: React.FC = ({token}) => {
{ return (
-
+

Yearn Asset Repo Upload

- +
); diff --git a/app/image-tools/src/index.css b/app/image-tools/src/index.css index d44c0a84d6..49b897574c 100644 --- a/app/image-tools/src/index.css +++ b/app/image-tools/src/index.css @@ -3,7 +3,7 @@ @tailwind utilities; /* App base styles */ -html, body, #root { - height: 100%; +html, +body { + height: 100%; } - 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 index 9b39dd87d4..653b83682d 100644 --- a/app/image-tools/src/lib/chains.ts +++ b/app/image-tools/src/lib/chains.ts @@ -1,44 +1,22 @@ 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', + 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 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()); + return /^0x[a-fA-F0-9]{40}$/.test(addr.trim()); } diff --git a/app/image-tools/src/lib/githubAuth.test.ts b/app/image-tools/src/lib/githubAuth.test.ts new file mode 100644 index 0000000000..e2b0923608 --- /dev/null +++ b/app/image-tools/src/lib/githubAuth.test.ts @@ -0,0 +1,100 @@ +import {describe, expect, test} from 'bun:test'; +import {completeGithubOAuthSignIn, createGithubOAuthNonce} from './githubAuth'; + +describe('GitHub OAuth completion', () => { + test('stores a fragment token only for a pending flow with matching state', () => { + const auth = createTestAuth({pending: true, storedState: 'expected-state'}); + const result = completeGithubOAuthSignIn('#token=github-token&state=expected-state', auth.dependencies); + + expect(result).toEqual({ok: true}); + expect(auth.storedTokens).toEqual(['github-token']); + expect(auth.broadcasts).toBe(1); + expect(auth.clearedPending).toBe(1); + expect(auth.clearedState).toBe(1); + }); + + test('missing state never stores a token', () => { + const auth = createTestAuth({pending: true, storedState: 'expected-state'}); + const result = completeGithubOAuthSignIn('#token=attacker-token', auth.dependencies); + + expect(result).toEqual({ + ok: false, + error: 'GitHub sign-in could not be verified. Please start sign-in again.' + }); + expect(auth.storedTokens).toEqual([]); + }); + + test('mismatched state never stores a token', () => { + const auth = createTestAuth({pending: true, storedState: 'expected-state'}); + const result = completeGithubOAuthSignIn('#token=attacker-token&state=wrong-state', auth.dependencies); + + expect(result.ok).toBe(false); + expect(auth.storedTokens).toEqual([]); + }); + + test('a matching state without a pending sign-in never stores a token', () => { + const auth = createTestAuth({pending: false, storedState: 'expected-state'}); + const result = completeGithubOAuthSignIn('#token=attacker-token&state=expected-state', auth.dependencies); + + expect(result).toEqual({ + ok: false, + error: 'No GitHub sign-in attempt is pending. Please start sign-in again.' + }); + expect(auth.storedTokens).toEqual([]); + }); + + test('query-string token and state are not accepted', () => { + const auth = createTestAuth({pending: true, storedState: 'expected-state'}); + const result = completeGithubOAuthSignIn('?token=attacker-token&state=expected-state', auth.dependencies); + + expect(result.ok).toBe(false); + expect(auth.storedTokens).toEqual([]); + }); +}); + +test('OAuth nonce uses a 160-bit browser crypto value encoded as hex', () => { + const first = createGithubOAuthNonce(); + const second = createGithubOAuthNonce(); + + expect(first).toMatch(/^[a-f0-9]{40}$/); + expect(second).toMatch(/^[a-f0-9]{40}$/); + expect(second).not.toBe(first); +}); + +function createTestAuth({pending, storedState}: {pending: boolean; storedState: string | null}) { + const state = { + storedTokens: [] as string[], + broadcasts: 0, + clearedPending: 0, + clearedState: 0 + }; + + return { + get storedTokens() { + return state.storedTokens; + }, + get broadcasts() { + return state.broadcasts; + }, + get clearedPending() { + return state.clearedPending; + }, + get clearedState() { + return state.clearedState; + }, + dependencies: { + readStoredState: () => storedState, + readAuthPending: () => pending, + storeAuthToken: (token: string) => state.storedTokens.push(token), + clearStoredState: () => { + state.clearedState += 1; + }, + clearAuthPending: () => { + state.clearedPending += 1; + }, + broadcastAuthChange: () => { + state.broadcasts += 1; + } + } + }; +} diff --git a/app/image-tools/src/lib/githubAuth.ts b/app/image-tools/src/lib/githubAuth.ts index 5b01315e3d..c0d01e6f0f 100644 --- a/app/image-tools/src/lib/githubAuth.ts +++ b/app/image-tools/src/lib/githubAuth.ts @@ -4,10 +4,27 @@ export const AUTH_CHANGE_EVENT = 'github-auth-changed'; export const AUTH_PENDING_STORAGE_KEY = 'github_oauth_pending'; const DEFAULT_OAUTH_BROKER_ORIGIN = 'https://token-assets.yearn.fi'; +type GithubOAuthCompletionDependencies = { + readStoredState: () => string | null; + readAuthPending: () => boolean; + storeAuthToken: (token: string) => void; + clearStoredState: () => void; + clearAuthPending: () => void; + broadcastAuthChange: () => void; +}; + +export type GithubOAuthCompletionResult = {ok: true} | {ok: false; error: string}; + +export function createGithubOAuthNonce(): string { + const bytes = new Uint8Array(20); + crypto.getRandomValues(bytes); + return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); +} + export function buildAuthorizeUrl( state: string, returnTo = window.location.origin, - brokerOrigin = import.meta.env.VITE_GITHUB_OAUTH_BROKER_ORIGIN || DEFAULT_OAUTH_BROKER_ORIGIN + brokerOrigin = process.env.NEXT_PUBLIC_GITHUB_OAUTH_BROKER_ORIGIN || DEFAULT_OAUTH_BROKER_ORIGIN ) { const url = new URL('/api/auth/github/start', brokerOrigin); url.searchParams.set('state', state); @@ -92,3 +109,43 @@ export function broadcastAuthChange() { window.dispatchEvent(new StorageEvent('storage', {key: TOKEN_STORAGE_KEY})); } catch {} } + +export function completeGithubOAuthSignIn( + fragment: string, + dependencies: GithubOAuthCompletionDependencies = { + readStoredState, + readAuthPending, + storeAuthToken, + clearStoredState, + clearAuthPending, + broadcastAuthChange + } +): GithubOAuthCompletionResult { + try { + if (!dependencies.readAuthPending()) { + return {ok: false, error: 'No GitHub sign-in attempt is pending. Please start sign-in again.'}; + } + if (!fragment.startsWith('#')) { + return {ok: false, error: 'GitHub returned an invalid sign-in response. Please try again.'}; + } + + const params = new URLSearchParams(fragment.slice(1)); + const token = params.get('token'); + const returnedState = params.get('state'); + const storedState = dependencies.readStoredState(); + + if (!token) { + return {ok: false, error: 'GitHub did not return an access token. Please try again.'}; + } + if (!returnedState || !storedState || returnedState !== storedState) { + return {ok: false, error: 'GitHub sign-in could not be verified. Please start sign-in again.'}; + } + + dependencies.storeAuthToken(token); + dependencies.broadcastAuthChange(); + return {ok: true}; + } finally { + dependencies.clearAuthPending(); + dependencies.clearStoredState(); + } +} 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 079ff3b48d..0000000000 --- a/app/image-tools/src/routes/auth/github-success.tsx +++ /dev/null @@ -1,38 +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.hash.slice(1) || window.location.search); - const token = params.get('token'); - const state = params.get('state'); - const returnedState = readStoredState(); - clearAuthPending(); - const stateMatches = Boolean(state && returnedState && state === returnedState); - if (!stateMatches) { - console.warn('OAuth state mismatch.'); - } else 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 index bbdd5e3528..aacdd8c54b 100644 --- a/app/image-tools/src/routes/upload.tsx +++ b/app/image-tools/src/routes/upload.tsx @@ -1,5 +1,6 @@ +'use client'; + import React, {useEffect, useMemo, useState} from 'react'; -import {createRoute} from '@tanstack/react-router'; import {Switch} from '@headlessui/react'; import {AssetDropzone} from '../components/AssetDropzone'; import {PreviewGrid} from '../components/PreviewGrid'; @@ -7,8 +8,7 @@ import {PrReviewDialog} from '../components/PrReviewDialog'; import {SegmentedToggle} from '../components/SegmentedToggle'; import {StatusBanner, type StatusTone} from '../components/StatusBanner'; import {useGithubAuthToken} from '../hooks/useGithubAuth'; -import {API_BASE_URL} from '../lib/api'; -import {getRpcUrl, isEvmAddress, listKnownChains} from '../lib/chains'; +import {isEvmAddress, listKnownChains} from '../lib/chains'; import { type AssetFileKind, type AssetFiles, @@ -19,7 +19,6 @@ import { revokePreviewUrl } from '../lib/imagePreview'; import {clearUploadDraft, readUploadDraft, saveUploadDraft} from '../lib/uploadDraft'; -import {rootRoute} from '../router'; type TokenItem = { id: string; @@ -54,7 +53,7 @@ type StatusState = { prUrl?: string; }; -type UploadUrlParams = { +export type UploadUrlParams = { mode?: 'token' | 'chain'; chainId?: string; address?: string; @@ -91,7 +90,11 @@ function createChainItem(): ChainItem { }; } -export const UploadComponent: React.FC = () => { +type UploadComponentProps = { + initialParams?: UploadUrlParams; +}; + +export const UploadComponent: React.FC = ({initialParams = {}}) => { const token = useGithubAuthToken(); const [mode, setMode] = useState<'token' | 'chain'>('token'); const [chainItems, setChainItems] = useState(() => [createChainItem()]); @@ -169,7 +172,7 @@ export const UploadComponent: React.FC = () => { useEffect(() => { let cancelled = false; - const urlParams = readUploadUrlParams(); + const urlParams = initialParams; const restoreDraft = async () => { try { @@ -217,7 +220,7 @@ export const UploadComponent: React.FC = () => { return () => { cancelled = true; }; - }, []); + }, [initialParams]); useEffect(() => { if (!draftReady) return; @@ -326,14 +329,17 @@ export const UploadComponent: React.FC = () => { setStatus({tone: 'info', title: 'Creating PR', message: 'Uploading images and preparing a GitHub branch.'}); try { - const reqUrl = new URL('/api/upload', API_BASE_URL).toString(); const form = await buildFormData({ mode, chainItems, tokenItems, prMeta: {title: prTitle, body: prBody} }); - const res = await fetch(reqUrl, {method: 'POST', headers: {Authorization: `Bearer ${token}`}, body: form}); + const res = await fetch('/api/upload', { + method: 'POST', + headers: {Authorization: `Bearer ${token}`}, + body: form + }); if (!res.ok) { setStatus({tone: 'error', title: 'Upload failed', message: await readApiError(res)}); return; @@ -848,27 +854,6 @@ function restoreChainDraftItems(draft: Awaited>, @@ -915,8 +900,7 @@ async function fetchErc20Name(chainIdStr: string, address: string): Promise= 192) { - const lenHex = hex.slice(64, 128); - const len = parseInt(lenHex || '0', 16); - const dataHex = hex.slice(128, 128 + len * 2); - return hexToUtf8(dataHex); + throw new Error('Token name was not returned'); + } catch (error) { + throw error instanceof Error ? error : new Error('Token name lookup failed'); } - if (hex.length === 64) return hexToUtf8(hex.replace(/00+$/, '')); - return hexToUtf8(hex); -} - -function hexToUtf8(hex: string): string { - const bytes = hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []; - return new TextDecoder().decode(new Uint8Array(bytes)).replace(/\u0000+$/, ''); } function buildDefaultPrMetadata(mode: 'token' | 'chain', tokenItems: TokenItem[], chainItems: ChainItem[]): PrMetadata { if (mode === 'chain') { const chains = chainItems.map(item => item.chainId).filter(Boolean); const paths = chainItems.flatMap(item => [ - `/chain/${item.chainId}/logo.svg`, - `/chain/${item.chainId}/logo-32.png`, - `/chain/${item.chainId}/logo-128.png` + `/chains/${item.chainId}/logo.svg`, + `/chains/${item.chainId}/logo-32.png`, + `/chains/${item.chainId}/logo-128.png` ]); return { title: `feat: add chain assets (${chains.length})`, @@ -986,9 +934,9 @@ function buildDefaultPrMetadata(mode: 'token' | 'chain', tokenItems: TokenItem[] const chains = tokenItems.map(item => item.chainId).filter(Boolean); const uniqueChains = Array.from(new Set(chains)); const paths = tokenItems.flatMap(item => [ - `/token/${item.chainId}/${item.address.toLowerCase()}/logo.svg`, - `/token/${item.chainId}/${item.address.toLowerCase()}/logo-32.png`, - `/token/${item.chainId}/${item.address.toLowerCase()}/logo-128.png` + `/tokens/${item.chainId}/${item.address.toLowerCase()}/logo.svg`, + `/tokens/${item.chainId}/${item.address.toLowerCase()}/logo-32.png`, + `/tokens/${item.chainId}/${item.address.toLowerCase()}/logo-128.png` ]); return { @@ -1064,9 +1012,3 @@ async function readApiError(res: Response) { return `HTTP ${res.status}`; } } - -export const UploadRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/', - component: UploadComponent -}); diff --git a/app/image-tools/src/server/erc20Name.ts b/app/image-tools/src/server/erc20Name.ts new file mode 100644 index 0000000000..39af1e4d48 --- /dev/null +++ b/app/image-tools/src/server/erc20Name.ts @@ -0,0 +1,108 @@ +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 keys = [`RPC_URI_FOR_${chainId}`, `RPC_${chainId}`, `VITE_RPC_URI_FOR_${chainId}`, `VITE_RPC_${chainId}`]; + const val = keys.map(key => process.env[key]).find(Boolean); + 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 async function handleErc20Name(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/src/server/github.ts b/app/image-tools/src/server/github.ts new file mode 100644 index 0000000000..0eb4009ec0 --- /dev/null +++ b/app/image-tools/src/server/github.ts @@ -0,0 +1,373 @@ +export 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/auth/github/callback.ts b/app/image-tools/src/server/githubOAuthCallback.ts similarity index 80% rename from app/image-tools/api/auth/github/callback.ts rename to app/image-tools/src/server/githubOAuthCallback.ts index a0f13c1281..f4981cd7d1 100644 --- a/app/image-tools/api/auth/github/callback.ts +++ b/app/image-tools/src/server/githubOAuthCallback.ts @@ -1,8 +1,6 @@ -export const config = {runtime: 'edge'}; +import {readOAuthState, resolveOAuthReturnTo} from './githubOAuthState'; -import {readOAuthStateWithLegacyProductionFallback, resolveOAuthReturnTo} from '../../../src/server/githubOAuthState'; - -export default async function (req: Request): Promise { +export async function handleGithubOAuthCallback(req: Request): Promise { try { const url = new URL(req.url); const code = url.searchParams.get('code'); @@ -14,7 +12,7 @@ export default async function (req: Request): Promise { }); } - const clientId = process.env.GITHUB_CLIENT_ID || process.env.VITE_GITHUB_CLIENT_ID; + const clientId = process.env.GITHUB_CLIENT_ID; const clientSecret = process.env.GITHUB_CLIENT_SECRET; if (!clientId || !clientSecret) { return new Response(JSON.stringify({error: 'Missing GitHub OAuth env vars'}), { @@ -22,10 +20,10 @@ export default async function (req: Request): Promise { headers: {'Content-Type': 'application/json'} }); } - let oauthState: Awaited>; + let oauthState: Awaited>; let returnTo: string; try { - oauthState = await readOAuthStateWithLegacyProductionFallback(state, clientSecret); + oauthState = await readOAuthState(state, clientSecret); returnTo = resolveOAuthReturnTo(oauthState.returnTo, process.env.OAUTH_RETURN_ORIGINS || ''); } catch (error: any) { return new Response(JSON.stringify({error: error?.message || 'Invalid OAuth state'}), { diff --git a/app/image-tools/api/auth/github/start.ts b/app/image-tools/src/server/githubOAuthStart.ts similarity index 78% rename from app/image-tools/api/auth/github/start.ts rename to app/image-tools/src/server/githubOAuthStart.ts index 7ab605efd2..717cbddf52 100644 --- a/app/image-tools/api/auth/github/start.ts +++ b/app/image-tools/src/server/githubOAuthStart.ts @@ -1,14 +1,12 @@ -export const config = {runtime: 'edge'}; +import {createOAuthState, resolveOAuthReturnTo} from './githubOAuthState'; -import {createOAuthState, resolveOAuthReturnTo} from '../../../src/server/githubOAuthState'; - -export default async function (req: Request): Promise { +export async function handleGithubOAuthStart(req: Request): Promise { try { if (req.method !== 'GET') return new Response('Method Not Allowed', {status: 405}); const requestUrl = new URL(req.url); const nonce = requestUrl.searchParams.get('state') || ''; - const clientId = process.env.GITHUB_CLIENT_ID || process.env.VITE_GITHUB_CLIENT_ID; + const clientId = process.env.GITHUB_CLIENT_ID; const clientSecret = process.env.GITHUB_CLIENT_SECRET; if (!clientId || !clientSecret) throw new Error('Missing GitHub OAuth env vars'); diff --git a/app/image-tools/src/server/githubOAuthState.test.ts b/app/image-tools/src/server/githubOAuthState.test.ts new file mode 100644 index 0000000000..b14703195e --- /dev/null +++ b/app/image-tools/src/server/githubOAuthState.test.ts @@ -0,0 +1,40 @@ +import {describe, expect, test} from 'bun:test'; +import {createOAuthState, readOAuthState, resolveOAuthReturnTo} from './githubOAuthState'; + +const SECRET = 'test-client-secret'; +const NOW = 1_800_000_000_000; +const NONCE = 'abcdefghijklmnopqrst'; + +describe('GitHub OAuth state', () => { + test('round trips a signed state payload', async () => { + const state = await createOAuthState(NONCE, 'https://dev-vm.tail197cc7.ts.net:8446', SECRET, NOW); + const payload = await readOAuthState(state, SECRET, NOW + 1_000); + + expect(payload).toEqual({ + nonce: NONCE, + returnTo: 'https://dev-vm.tail197cc7.ts.net:8446', + expiresAt: NOW + 10 * 60 * 1_000 + }); + }); + + test('accepts canonical, trusted preview, and configured exact origins', () => { + expect(resolveOAuthReturnTo('https://token-assets.yearn.fi/path')).toBe('https://token-assets.yearn.fi'); + expect(resolveOAuthReturnTo('https://dev-vm.tail197cc7.ts.net:8446/path')).toBe( + 'https://dev-vm.tail197cc7.ts.net:8446' + ); + expect(resolveOAuthReturnTo('http://localhost:3000/path', 'http://localhost:3000')).toBe( + 'http://localhost:3000' + ); + }); + + test('rejects unapproved origins and expired or tampered state', async () => { + expect(() => resolveOAuthReturnTo('https://attacker.example')).toThrow('Untrusted OAuth return URL'); + const state = await createOAuthState(NONCE, 'https://token-assets.yearn.fi', SECRET, NOW); + await expect(readOAuthState(state, SECRET, NOW + 11 * 60 * 1_000)).rejects.toThrow('Expired OAuth state'); + await expect(readOAuthState(`${state}x`, SECRET, NOW)).rejects.toThrow('Invalid OAuth state signature'); + }); + + test('rejects the legacy unsigned production state', async () => { + await expect(readOAuthState(NONCE, SECRET, NOW)).rejects.toThrow('Invalid OAuth state'); + }); +}); diff --git a/app/image-tools/src/server/githubOAuthState.ts b/app/image-tools/src/server/githubOAuthState.ts index c48846b501..a16ce13660 100644 --- a/app/image-tools/src/server/githubOAuthState.ts +++ b/app/image-tools/src/server/githubOAuthState.ts @@ -65,17 +65,6 @@ export async function readOAuthState(state: string, secret: string, now = Date.n return payload; } -export async function readOAuthStateWithLegacyProductionFallback( - state: string, - secret: string, - now = Date.now() -): Promise { - if (NONCE_PATTERN.test(state)) { - return {nonce: state, returnTo: CANONICAL_APP_ORIGIN, expiresAt: now}; - } - return readOAuthState(state, secret, now); -} - async function sign(value: string, secret: string): Promise { return crypto.subtle.sign('HMAC', await importSigningKey(secret), new TextEncoder().encode(value)); } diff --git a/app/image-tools/src/server/health.ts b/app/image-tools/src/server/health.ts new file mode 100644 index 0000000000..48d7561713 --- /dev/null +++ b/app/image-tools/src/server/health.ts @@ -0,0 +1,6 @@ +export async function handleHealth(): 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/src/server/ping.ts b/app/image-tools/src/server/ping.ts new file mode 100644 index 0000000000..2301ad12fa --- /dev/null +++ b/app/image-tools/src/server/ping.ts @@ -0,0 +1,6 @@ +export async function handlePing(): Promise { + return new Response(JSON.stringify({ok: true, service: 'image-tools'}), { + status: 200, + headers: {'Content-Type': 'application/json'} + }); +} diff --git a/app/image-tools/src/server/upload.test.ts b/app/image-tools/src/server/upload.test.ts new file mode 100644 index 0000000000..3d553ed022 --- /dev/null +++ b/app/image-tools/src/server/upload.test.ts @@ -0,0 +1,127 @@ +import {afterEach, describe, expect, test} from 'bun:test'; +import {buildChainAssetPaths, handleUpload, resolveTargetRepo, validateChainId} from './upload'; + +const ENV_KEYS = ['REPO_OWNER', 'REPO_NAME', 'ALLOW_REPO_OVERRIDE'] as const; +const originalEnv = Object.fromEntries(ENV_KEYS.map(key => [key, process.env[key]])); + +afterEach(() => { + for (const key of ENV_KEYS) { + const value = originalEnv[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } +}); + +describe('upload repository targeting', () => { + test('targets yearn/tokenAssets unless overrides are explicitly enabled', () => { + process.env.REPO_OWNER = 'personal-fork'; + process.env.REPO_NAME = 'different-repo'; + delete process.env.ALLOW_REPO_OVERRIDE; + + expect(resolveTargetRepo()).toEqual({owner: 'yearn', repo: 'tokenAssets'}); + }); + + test('honors an intentional repository override', () => { + process.env.REPO_OWNER = 'personal-fork'; + process.env.REPO_NAME = 'different-repo'; + process.env.ALLOW_REPO_OVERRIDE = 'true'; + + expect(resolveTargetRepo()).toEqual({owner: 'personal-fork', repo: 'different-repo'}); + }); +}); + +test('chain 999 creates Git tree paths without requiring a directory entry', () => { + expect(validateChainId('999')).toBe('999'); + expect(buildChainAssetPaths('999')).toEqual([ + 'chains/999/logo.svg', + 'chains/999/logo-32.png', + 'chains/999/logo-128.png' + ]); +}); + +test('the documented btcm chain identifier is accepted', () => { + expect(validateChainId('btcm')).toBe('btcm'); + expect(buildChainAssetPaths('btcm')).toEqual([ + 'chains/btcm/logo.svg', + 'chains/btcm/logo-32.png', + 'chains/btcm/logo-128.png' + ]); +}); + +const INVALID_CHAIN_IDS = [ + '', + '../../.github/workflows', + '999/../../foo', + '.', + '..', + '1\\foo', + ' 999 ', + '\t999', + '%2e%2e%2f.github', + '999%2Ffoo', + 'arbitrum' +]; + +describe('chainId path validation', () => { + for (const chainId of INVALID_CHAIN_IDS) { + test(`rejects ${JSON.stringify(chainId)} for token and chain uploads before GitHub`, async () => { + let githubCalls = 0; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => { + githubCalls += 1; + return new Response('{}', {status: 500}); + }) as unknown as typeof fetch; + + try { + for (const target of ['token', 'chain'] as const) { + const response = await handleUpload(buildUploadRequest(target, chainId)); + const body = (await response.json()) as {error?: string}; + + expect(response.status).toBe(400); + expect(body.error).toContain(chainId ? 'Invalid chainId' : 'Missing chainId'); + if (chainId) expect(body.error).toContain('expected decimal digits or "btcm"'); + } + expect(githubCalls).toBe(0); + } finally { + globalThis.fetch = originalFetch; + } + }); + } +}); + +function buildUploadRequest(target: 'token' | 'chain', chainId: string): Request { + const itemId = 'security-test'; + const form = new FormData(); + form.set('target', target); + form.set( + 'items', + JSON.stringify([ + { + id: itemId, + chainId, + ...(target === 'token' ? {address: '0x0000000000000000000000000000000000000001'} : {}) + } + ]) + ); + form.set( + `svg_${itemId}`, + new File([''], 'logo.svg', {type: 'image/svg+xml'}) + ); + form.set(`png32_${itemId}`, pngFile(32, 'logo-32.png')); + form.set(`png128_${itemId}`, pngFile(128, 'logo-128.png')); + + return new Request('http://localhost/api/upload', { + method: 'POST', + headers: {Authorization: 'Bearer test-token'}, + body: form + }); +} + +function pngFile(size: number, name: string): File { + const bytes = new Uint8Array(25); + bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + const view = new DataView(bytes.buffer); + view.setUint32(16, size); + view.setUint32(20, size); + return new File([bytes], name, {type: 'image/png'}); +} diff --git a/app/image-tools/api/upload.ts b/app/image-tools/src/server/upload.ts similarity index 80% rename from app/image-tools/api/upload.ts rename to app/image-tools/src/server/upload.ts index b113e2007d..3295273e57 100644 --- a/app/image-tools/api/upload.ts +++ b/app/image-tools/src/server/upload.ts @@ -1,4 +1,4 @@ -import {openPrWithFilesForkAware, getUserLogin} from './github'; +import {getUserLogin, openPrWithFilesForkAware} from './github'; const CANONICAL_OWNER = 'yearn'; const CANONICAL_REPO = 'tokenAssets'; @@ -22,23 +22,25 @@ function validationError(message: string): never { throw new UploadValidationError(message); } +const CHAIN_ID_PATTERN = /^(?:\d+|btcm)$/; + +export function validateChainId(chainId: string, subject = 'asset'): string { + if (!chainId) validationError(`Missing chainId for ${subject}`); + if (!CHAIN_ID_PATTERN.test(chainId)) { + validationError(`Invalid chainId for ${subject}; expected decimal digits or "btcm"`); + } + return chainId; +} + // 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} { +export 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; + const owner = allowOverride && envOwner ? envOwner : CANONICAL_OWNER; + const repo = allowOverride && envRepo ? envRepo : CANONICAL_REPO; return {owner, repo}; } @@ -86,7 +88,7 @@ function toBase64(bytes: Uint8Array): string { const sub = bytes.subarray(i, i + chunk); binary += String.fromCharCode(...sub); } - // btoa is available in Edge runtime + // Encode in chunks to avoid exceeding the function argument limit for larger images. return btoa(binary); } @@ -94,6 +96,20 @@ function jsonResponse(body: unknown, status: number) { return new Response(JSON.stringify(body), {status, headers: {'Content-Type': 'application/json'}}); } +export function buildChainAssetPaths(chainId: string): [string, string, string] { + validateChainId(chainId, 'chain asset'); + return [`chains/${chainId}/logo.svg`, `chains/${chainId}/logo-32.png`, `chains/${chainId}/logo-128.png`]; +} + +function buildTokenAssetPaths(chainId: string, address: string): [string, string, string] { + validateChainId(chainId, 'token'); + return [ + `tokens/${chainId}/${address}/logo.svg`, + `tokens/${chainId}/${address}/logo-32.png`, + `tokens/${chainId}/${address}/logo-128.png` + ]; +} + function parseTokenItems(form: FormData, globalChainId: string): TokenUploadItem[] { const manifest = String(form.get('items') || '').trim(); if (manifest) { @@ -106,7 +122,7 @@ function parseTokenItems(form: FormData, globalChainId: string): TokenUploadItem if (!Array.isArray(parsed)) validationError('items must be an array'); return parsed.map((item, index) => ({ id: String(item.id || '').trim() || String(index), - chainId: String(item.chainId || globalChainId || '').trim(), + chainId: String(item.chainId ?? globalChainId ?? ''), address: String(item.address || '').trim() })); } @@ -114,7 +130,7 @@ function parseTokenItems(form: FormData, globalChainId: string): TokenUploadItem const addresses = (form.getAll('address') as string[]).map(address => String(address || '').trim()).filter(Boolean); return addresses.map((address, index) => ({ id: String(index), - chainId: String(form.get(`chainId_${index}`) || globalChainId || '').trim(), + chainId: String(form.get(`chainId_${index}`) || globalChainId || ''), address })); } @@ -131,7 +147,7 @@ function parseChainItems(form: FormData, globalChainId: string): ChainUploadItem if (!Array.isArray(parsed)) validationError('items must be an array'); return parsed.map((item, index) => ({ id: String(item.id || '').trim() || String(index), - chainId: String(item.chainId || globalChainId || '').trim() + chainId: String(item.chainId ?? globalChainId ?? '') })); } @@ -172,7 +188,7 @@ export async function handleUpload(req: Request): Promise { const form = await req.formData(); const target = String(form.get('target') || 'token'); - const globalChainId = String(form.get('chainId') || '').trim(); + const globalChainId = String(form.get('chainId') || ''); const prTitleOverride = String(form.get('prTitle') || '').trim(); const prBodyOverride = String(form.get('prBody') || '').trim(); @@ -183,22 +199,23 @@ export async function handleUpload(req: Request): Promise { const items = parseTokenItems(form, globalChainId); if (!items.length) validationError('At least one address required for token uploads'); for (const item of items) { - if (!item.chainId) validationError(`Missing chainId for token ${item.id}`); + const chainId = validateChainId(item.chainId, `token ${item.id}`); if (!isEvmAddress(item.address)) validationError(`Invalid EVM address for token ${item.id}`); const {svgBytes, png32Bytes, png128Bytes} = await readRequiredAssetFiles(form, item.id); const addrLower = item.address.toLowerCase(); + const [svgPath, png32Path, png128Path] = buildTokenAssetPaths(chainId, addrLower); prFiles.push( { - path: ['tokens', String(item.chainId), addrLower, 'logo.svg'].join('/'), + path: svgPath, contentBase64: toBase64(svgBytes) }, { - path: ['tokens', String(item.chainId), addrLower, 'logo-32.png'].join('/'), + path: png32Path, contentBase64: toBase64(png32Bytes) }, { - path: ['tokens', String(item.chainId), addrLower, 'logo-128.png'].join('/'), + path: png128Path, contentBase64: toBase64(png128Bytes) } ); @@ -209,7 +226,7 @@ export async function handleUpload(req: Request): Promise { const seenChainIds = new Set(); for (const item of items) { - if (!item.chainId) validationError(`Missing chainId for chain asset ${item.id}`); + validateChainId(item.chainId, `chain asset ${item.id}`); if (seenChainIds.has(item.chainId)) validationError(`Duplicate chainId for chain asset ${item.chainId}`); seenChainIds.add(item.chainId); @@ -217,15 +234,16 @@ export async function handleUpload(req: Request): Promise { for (const item of items) { const {svgBytes, png32Bytes, png128Bytes} = await readRequiredAssetFiles(form, item.id); + const [svgPath, png32Path, png128Path] = buildChainAssetPaths(item.chainId); prFiles.push( - {path: ['chains', String(item.chainId), 'logo.svg'].join('/'), contentBase64: toBase64(svgBytes)}, + {path: svgPath, contentBase64: toBase64(svgBytes)}, { - path: ['chains', String(item.chainId), 'logo-32.png'].join('/'), + path: png32Path, contentBase64: toBase64(png32Bytes) }, { - path: ['chains', String(item.chainId), 'logo-128.png'].join('/'), + path: png128Path, contentBase64: toBase64(png128Bytes) } ); @@ -248,9 +266,9 @@ export async function handleUpload(req: Request): Promise { 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` + `/tokens/${chainsForBody[i]}/${addr}/logo.svg`, + `/tokens/${chainsForBody[i]}/${addr}/logo-32.png`, + `/tokens/${chainsForBody[i]}/${addr}/logo-128.png` ]); prBody ||= [ `Chains: ${uniqueChains.join(', ')}`, @@ -264,9 +282,9 @@ export async function handleUpload(req: Request): Promise { const chainsForBody: string[] = items.map(item => item.chainId); prTitle ||= `feat: add chain assets (${chainsForBody.length})`; const directoryLocations = chainsForBody.flatMap(chainId => [ - `/chain/${chainId}/logo.svg`, - `/chain/${chainId}/logo-32.png`, - `/chain/${chainId}/logo-128.png` + `/chains/${chainId}/logo.svg`, + `/chains/${chainId}/logo-32.png`, + `/chains/${chainId}/logo-128.png` ]); prBody ||= [ `Chains: ${chainsForBody.join(', ')}`, @@ -294,5 +312,3 @@ export async function handleUpload(req: Request): Promise { return jsonResponse({error: e?.message || 'Upload failed'}, e?.status || 500); } } - -export default {fetch: handleUpload}; diff --git a/app/image-tools/src/vite-env.d.ts b/app/image-tools/src/vite-env.d.ts deleted file mode 100644 index ed77210660..0000000000 --- a/app/image-tools/src/vite-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// - diff --git a/app/image-tools/tailwind.config.ts b/app/image-tools/tailwind.config.ts index 315ce822b9..e8ada17f57 100644 --- a/app/image-tools/tailwind.config.ts +++ b/app/image-tools/tailwind.config.ts @@ -1,10 +1,9 @@ -import type { Config } from 'tailwindcss'; +import type {Config} from 'tailwindcss'; export default { - content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'], - theme: { - extend: {}, - }, - plugins: [], + content: ['./app/**/*.{ts,tsx,js,jsx}', './src/**/*.{ts,tsx,js,jsx}'], + theme: { + extend: {} + }, + plugins: [] } satisfies Config; - diff --git a/app/image-tools/tsconfig.json b/app/image-tools/tsconfig.json index a15c4392f2..373b299ee8 100644 --- a/app/image-tools/tsconfig.json +++ b/app/image-tools/tsconfig.json @@ -1,19 +1,28 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "types": ["node"] - }, - "include": ["src", "api"] + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + }, + "allowJs": true + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/app/image-tools/vercel.json b/app/image-tools/vercel.json index 33dc7970ea..10d43a7d09 100644 --- a/app/image-tools/vercel.json +++ b/app/image-tools/vercel.json @@ -1,9 +1,5 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "functions": { - "api/upload.ts": { - "maxDuration": 60 - } - }, - "rewrites": [{"source": "/(.*)", "destination": "/"}] + "installCommand": "bun install --frozen-lockfile", + "buildCommand": "bun run build" } diff --git a/app/image-tools/vite.config.ts b/app/image-tools/vite.config.ts deleted file mode 100644 index eb0f249a32..0000000000 --- a/app/image-tools/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {defineConfig} from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - allowedHosts: ['dev-vm.tail197cc7.ts.net'] - }, - preview: { - allowedHosts: ['dev-vm.tail197cc7.ts.net'] - } -});