From b115ad99b1ad9c708956f71c4fe5858f07be3951 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 12:05:44 +0000 Subject: [PATCH] Build Zambia crypto ramp MVP Co-authored-by: Emmanuel Mangalashi --- .dockerignore | 7 + .env.example | 41 + .gitignore | 10 + Dockerfile | 28 + README.md | 58 + components.json | 19 + docker-compose.yml | 36 + docs/ARCHITECTURE.md | 160 + next-env.d.ts | 6 + next.config.ts | 14 + package-lock.json | 4843 +++++++++++++++++ package.json | 54 + postcss.config.mjs | 7 + prisma.config.ts | 14 + .../20260518114700_initial/migration.sql | 200 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 191 + prisma/seed.ts | 26 + public/.gitkeep | 1 + src/app/admin/page.tsx | 5 + src/app/api/admin/analytics/route.ts | 28 + src/app/api/admin/liquidity/route.ts | 25 + src/app/api/admin/login/route.ts | 17 + .../admin/transactions/[publicId]/route.ts | 47 + src/app/api/admin/transactions/route.ts | 36 + src/app/api/buy/route.ts | 99 + src/app/api/health/route.ts | 5 + src/app/api/quote/route.ts | 60 + src/app/api/sell/route.ts | 91 + .../transactions/[publicId]/deposit/route.ts | 91 + src/app/api/transactions/[publicId]/route.ts | 32 + src/app/api/transactions/route.ts | 43 + .../webhooks/mobile-money/[provider]/route.ts | 126 + src/app/buy/page.tsx | 5 + src/app/faq/page.tsx | 29 + src/app/globals.css | 74 + src/app/layout.tsx | 33 + src/app/page.tsx | 89 + src/app/sell/page.tsx | 5 + src/app/support/page.tsx | 32 + src/app/transactions/page.tsx | 5 + src/components/admin-dashboard.tsx | 126 + src/components/mobile-nav.tsx | 28 + src/components/providers.tsx | 39 + src/components/quote-summary.tsx | 42 + src/components/ramp-form.tsx | 195 + src/components/site-header.tsx | 37 + src/components/theme-toggle.tsx | 27 + src/components/transaction-history.tsx | 67 + src/components/ui/alert.tsx | 7 + src/components/ui/badge.tsx | 23 + src/components/ui/button.tsx | 46 + src/components/ui/card.tsx | 27 + src/components/ui/input.tsx | 18 + src/components/ui/label.tsx | 12 + src/components/ui/select.tsx | 25 + src/components/wallet-connect-button.tsx | 30 + src/lib/admin-auth.ts | 47 + src/lib/api.ts | 27 + src/lib/audit.ts | 33 + src/lib/blockchain/chains.ts | 17 + src/lib/blockchain/hot-wallet.ts | 74 + src/lib/blockchain/verify.ts | 39 + src/lib/constants.ts | 43 + src/lib/env.ts | 31 + src/lib/idempotency.ts | 25 + src/lib/payments/http-provider.ts | 67 + src/lib/payments/index.ts | 10 + src/lib/payments/mock.ts | 42 + src/lib/payments/types.ts | 35 + src/lib/prisma.ts | 19 + src/lib/queue.ts | 19 + src/lib/rates.ts | 93 + src/lib/risk.ts | 28 + src/lib/security.ts | 41 + src/lib/state-machine.ts | 32 + src/lib/utils.ts | 24 + src/lib/validators.ts | 41 + tsconfig.json | 41 + vercel.json | 5 + 80 files changed, 8277 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 components.json create mode 100644 docker-compose.yml create mode 100644 docs/ARCHITECTURE.md create mode 100644 next-env.d.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20260518114700_initial/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 public/.gitkeep create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/api/admin/analytics/route.ts create mode 100644 src/app/api/admin/liquidity/route.ts create mode 100644 src/app/api/admin/login/route.ts create mode 100644 src/app/api/admin/transactions/[publicId]/route.ts create mode 100644 src/app/api/admin/transactions/route.ts create mode 100644 src/app/api/buy/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/quote/route.ts create mode 100644 src/app/api/sell/route.ts create mode 100644 src/app/api/transactions/[publicId]/deposit/route.ts create mode 100644 src/app/api/transactions/[publicId]/route.ts create mode 100644 src/app/api/transactions/route.ts create mode 100644 src/app/api/webhooks/mobile-money/[provider]/route.ts create mode 100644 src/app/buy/page.tsx create mode 100644 src/app/faq/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/sell/page.tsx create mode 100644 src/app/support/page.tsx create mode 100644 src/app/transactions/page.tsx create mode 100644 src/components/admin-dashboard.tsx create mode 100644 src/components/mobile-nav.tsx create mode 100644 src/components/providers.tsx create mode 100644 src/components/quote-summary.tsx create mode 100644 src/components/ramp-form.tsx create mode 100644 src/components/site-header.tsx create mode 100644 src/components/theme-toggle.tsx create mode 100644 src/components/transaction-history.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/wallet-connect-button.tsx create mode 100644 src/lib/admin-auth.ts create mode 100644 src/lib/api.ts create mode 100644 src/lib/audit.ts create mode 100644 src/lib/blockchain/chains.ts create mode 100644 src/lib/blockchain/hot-wallet.ts create mode 100644 src/lib/blockchain/verify.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/env.ts create mode 100644 src/lib/idempotency.ts create mode 100644 src/lib/payments/http-provider.ts create mode 100644 src/lib/payments/index.ts create mode 100644 src/lib/payments/mock.ts create mode 100644 src/lib/payments/types.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/queue.ts create mode 100644 src/lib/rates.ts create mode 100644 src/lib/risk.ts create mode 100644 src/lib/security.ts create mode 100644 src/lib/state-machine.ts create mode 100644 src/lib/utils.ts create mode 100644 src/lib/validators.ts create mode 100644 tsconfig.json create mode 100644 vercel.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ffd3fc8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +.env +.env*.local +.git +coverage +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..53e1691 --- /dev/null +++ b/.env.example @@ -0,0 +1,41 @@ +# Core +NODE_ENV=development +NEXT_PUBLIC_APP_URL=http://localhost:3000 +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/afriramp?schema=public +REDIS_URL=redis://localhost:6379 + +# Admin auth +ADMIN_EMAIL=admin@afriramp.local +# Generate with: node -e "require('bcryptjs').hash('change-me', 12).then(console.log)" +ADMIN_PASSWORD_HASH= +ADMIN_JWT_SECRET=replace-with-32-byte-random-secret + +# Mobile money provider: mock | flutterwave | paychangu | lipila | pesapal +MOBILE_MONEY_PROVIDER=mock +MOBILE_MONEY_WEBHOOK_SECRET=replace-with-mobile-money-webhook-secret +MOBILE_MONEY_API_BASE_URL= +MOBILE_MONEY_API_KEY= +MOBILE_MONEY_API_SECRET= +MOBILE_MONEY_CALLBACK_URL=http://localhost:3000/api/webhooks/mobile-money/mock + +# Blockchain +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= +NEXT_PUBLIC_DEFAULT_CHAIN=polygon +ENABLE_BLOCKCHAIN_BROADCAST=false +HOT_WALLET_PRIVATE_KEY= +HOT_WALLET_ADDRESS= +DEPOSIT_WALLET_ADDRESSES= +POLYGON_RPC_URL=https://polygon-rpc.com +BASE_RPC_URL=https://mainnet.base.org +BNB_RPC_URL=https://bsc-dataseed.binance.org + +# Notifications +SMS_PROVIDER=mock +SMS_API_KEY= +EMAIL_FROM=receipts@afriramp.example +RESEND_API_KEY= + +# Risk controls +MAX_SINGLE_TRANSACTION_ZMW=25000 +QUOTE_LOCK_SECONDS=300 +REQUIRED_EVM_CONFIRMATIONS=12 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc2e67d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.env +.env*.local +.next +node_modules +dist +coverage +*.log +tsconfig.tsbuildinfo +prisma/dev.db +Dockerfile.local diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e36e95a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:22-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci + +FROM node:22-alpine AS builder +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 963760f..e107448 100644 --- a/README.md +++ b/README.md @@ -1 +1,59 @@ # AfriRamp + +Production-ready MVP for a non-custodial crypto on-ramp/off-ramp for Zambia. + +## What it supports + +- Buy crypto with MTN Mobile Money or Airtel Money +- Sell crypto for MTN Mobile Money or Airtel Money +- MetaMask wallet connection through Wagmi + Viem +- USDC on Base, USDT on Polygon, and WBTC on Polygon for MVP launch +- Fixed 3% platform fee shown before confirmation +- Prisma/PostgreSQL state machine, webhook replay protection, idempotency, audit logs, admin review, and freeze controls + +Native Bitcoin is modeled in the schema but intentionally disabled in the MVP. Use WBTC on Polygon first to keep wallet UX, fees, and verification simple. + +## Stack + +- Next.js 15+ app router +- TypeScript +- TailwindCSS + shadcn-style local UI primitives +- Prisma ORM + PostgreSQL +- Wagmi + Viem +- Redis-ready queue abstraction +- Docker and Vercel-ready configuration + +## Local setup + +```bash +cp .env.example .env +npm install +npm run prisma:generate +npm run prisma:migrate +npm run dev +``` + +For Postgres and Redis locally: + +```bash +docker compose up postgres redis +``` + +## Deployment + +1. Create a managed PostgreSQL database. +2. Configure the variables in `.env.example` in Vercel. +3. Run `npx prisma migrate deploy` during deployment or from a release command. +4. Configure your mobile money provider webhook to: + `/api/webhooks/mobile-money/{provider}` +5. Keep `ENABLE_BLOCKCHAIN_BROADCAST=false` until hot-wallet custody, treasury limits, and test transactions are reviewed. + +## Critical security notes + +- Never expose `HOT_WALLET_PRIVATE_KEY` to the browser. +- Use a dedicated low-balance hot wallet, multisig treasury, and strict refill procedures. +- Use provider-specific webhook signing secrets in production. +- Keep admin routes behind strong password hashes and ideally Vercel auth/VPN. +- Review failed, frozen, and manual-review transactions before releasing funds. + +See `docs/ARCHITECTURE.md` for the full architecture, API design, compliance notes, and build workflow. diff --git a/components.json b/components.json new file mode 100644 index 0000000..b0b4be7 --- /dev/null +++ b/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b211573 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + app: + build: . + depends_on: + - postgres + - redis + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/afriramp?schema=public + REDIS_URL: redis://redis:6379 + NEXT_PUBLIC_APP_URL: http://localhost:3000 + MOBILE_MONEY_PROVIDER: mock + MOBILE_MONEY_WEBHOOK_SECRET: local-webhook-secret + ADMIN_EMAIL: admin@afriramp.local + ADMIN_PASSWORD_HASH: "" + ADMIN_JWT_SECRET: local-admin-secret-change-me + ports: + - "3000:3000" + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: afriramp + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + +volumes: + postgres-data: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..bd067cc --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,160 @@ +# AfriRamp MVP Architecture + +## Product posture + +AfriRamp is non-custodial for users: users connect MetaMask, receive crypto directly to their wallet, and send crypto from their wallet when selling. The platform temporarily handles operational liquidity through hot wallets and mobile-money provider balances while a transaction is in flight. + +## Recommended MVP blockchain approach + +Launch with: + +1. **USDC on Base** - reliable stablecoin liquidity and low fees. +2. **USDT on Polygon** - broad user recognition and low fees. +3. **WBTC on Polygon** - Bitcoin exposure without native Bitcoin address management. + +Keep native Bitcoin disabled initially. Add it once you have unique deposit address generation, UTXO monitoring, confirmation policies, and treasury reconciliation. + +## Folder structure + +```txt +src/app Next.js app router pages and API routes +src/app/api/buy Buy transaction creation +src/app/api/sell Sell transaction creation +src/app/api/quote Real-time quote and 3% fee calculation +src/app/api/webhooks Mobile-money webhook handling +src/app/api/admin Admin authentication, analytics, review actions +src/components Mobile-first UI and shadcn-style primitives +src/lib/blockchain Viem chain, hot-wallet, and deposit verification helpers +src/lib/payments Swappable mobile-money provider abstraction +src/lib Prisma, auth, risk, rates, audit, security, state machine +prisma/schema.prisma PostgreSQL schema +``` + +## Database model summary + +- `User`: wallet-first profile, KYC-ready status, risk score, block flag. +- `Quote`: locked short-lived rate with fiat, crypto, fee, and expiry. +- `Transaction`: state machine for buy/sell, provider refs, chain refs, fees, freeze/failure fields. +- `WebhookEvent`: unique provider event IDs to prevent replay. +- `IdempotencyKey`: prevents duplicate transaction creation and duplicate payouts. +- `AuditLog`: immutable operational trail for user, webhook, system, and admin actions. +- `AdminSession`: future-ready admin session persistence. + +## API design + +| Route | Purpose | +| --- | --- | +| `POST /api/quote` | Creates a locked quote with exchange rate and 3% fee. | +| `POST /api/buy` | Creates a buy transaction and initiates mobile-money collection. | +| `POST /api/sell` | Creates a sell transaction and allocates a deposit address. | +| `GET /api/transactions` | Wallet-based transaction history. | +| `GET /api/transactions/:publicId` | Status tracking receipt. | +| `POST /api/transactions/:publicId/deposit` | Verifies sell-side EVM token deposit and triggers payout. | +| `POST /api/webhooks/mobile-money/:provider` | Verifies mobile-money webhook signatures and updates state. | +| `POST /api/admin/login` | HTTP-only cookie admin login. | +| `GET /api/admin/transactions` | Admin transaction review. | +| `PATCH /api/admin/transactions/:publicId` | Freeze, fail, or move to manual review. | +| `GET /api/admin/analytics` | Volume, completion, review, and 3% fee analytics. | +| `GET /api/admin/liquidity` | Hot-wallet token balance snapshot. | + +## Transaction state machine + +### Buy + +`QUOTE_CREATED -> PAYMENT_PENDING -> CRYPTO_DISBURSEMENT_PENDING -> COMPLETED` + +Failure/review states: + +`FAILED`, `FROZEN`, `EXPIRED`, `MANUAL_REVIEW` + +### Sell + +`QUOTE_CREATED -> CRYPTO_DEPOSIT_PENDING -> PAYOUT_PENDING -> COMPLETED` + +The deposit verification route validates sender, receiver, token contract, amount, transaction success, and confirmations before initiating mobile-money payout. + +## Mobile money provider abstraction + +`src/lib/payments/types.ts` defines: + +- `initiateCollection` +- `initiatePayout` +- `parseWebhook` + +The current implementation includes: + +- `mock` provider for local testing +- `HttpMobileMoneyProvider` for Lipila, Flutterwave, PayChangu, or Pesapal-style REST integrations + +Provider-specific payload mapping should be tightened once the selected provider contract is finalized. Keep provider-specific logic inside `src/lib/payments`, not in route handlers. + +## Security best practices implemented + +- Server-only private key access. +- Webhook HMAC verification hook. +- Unique provider webhook event IDs for replay prevention. +- Idempotency keys for transaction creation. +- Atomic status locking before crypto disbursement or mobile-money payout. +- Strict Zod validation for public API inputs. +- Wallet and Zambia mobile-number validation. +- Admin HTTP-only cookie with signed JWT. +- Full audit logging for user, system, webhook, and admin actions. +- Manual review and freeze statuses. + +## Security practices required before mainnet launch + +- Use provider-native webhook signature verification for the chosen mobile-money provider. +- Store secrets in Vercel encrypted environment variables or a dedicated secret manager. +- Use a dedicated hot wallet with low operational limits; keep treasury in multisig/cold storage. +- Add withdrawal limits, velocity rules, device fingerprinting, and sanctions/PEP screening hooks. +- Add background workers for retry queues rather than relying only on request lifecycle. +- Add automated reconciliation between provider ledger, database, and chain transactions. +- Use separate production, staging, and test wallets. + +## African fintech and Zambia compliance considerations + +- Engage Zambian counsel on Bank of Zambia, SEC, AML/CFT, data protection, and payment-service rules. +- Treat mobile money numbers as sensitive personal data. +- Build KYC thresholds from day one even if KYC collection is deferred. +- Maintain audit logs, transaction receipts, and suspicious-activity review notes. +- Keep language beginner-friendly: always show fees, rates, irreversible crypto warnings, and support channels. +- Design for intermittent network connectivity: small pages, clear retry states, and SMS/WhatsApp receipts. + +## Liquidity management + +- Start with narrow asset support and conservative limits. +- Keep a daily hot-wallet cap and refill manually from multisig. +- Maintain separate float for MTN and Airtel payout balances. +- Reconcile at least daily: database status, mobile-money settlement reports, hot-wallet balances, and fee revenue. +- Freeze transactions automatically when provider references, sender wallets, or phone numbers do not match expected values. + +## Fraud risk reduction + +- Lock quotes for short periods. +- Enforce idempotency and webhook replay protection. +- Block phone-prefix/payment-rail mismatches. +- Add velocity checks by phone number, wallet, IP, and device. +- Hold high-value, repeated, or mismatched transactions for manual review. +- Require enhanced KYC before raising limits. + +## Cursor AI coding workflow + +1. Build the schema and state machine first. +2. Implement quote, buy, sell, webhook, and deposit verification APIs. +3. Add the payment-provider abstraction with mock provider tests. +4. Add wallet UI and mobile-first flows. +5. Add admin dashboard and operational controls. +6. Add provider-specific integration once a mobile-money provider is selected. +7. Run `npm run typecheck`, `npm run build`, Prisma migration checks, and webhook simulations. +8. Deploy to staging with broadcast disabled, test end-to-end, then enable blockchain broadcasting with small limits. + +## Step-by-step build order for launch + +1. Select mobile money provider and finalize API contract. +2. Configure Postgres, Redis, Vercel, and secrets. +3. Run Prisma migrations. +4. Test mock buy/sell flows. +5. Test provider sandbox collections and payouts. +6. Test EVM token transfers on low-value mainnet amounts. +7. Configure monitoring, alerting, and reconciliation reports. +8. Launch with low limits and manual review for first transactions. diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..1434c91 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,14 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + experimental: { + serverActions: { + bodySizeLimit: "1mb" + } + }, + output: "standalone", + poweredByHeader: false, + reactStrictMode: true +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b81f585 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4843 @@ +{ + "name": "afriramp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "afriramp", + "version": "0.1.0", + "dependencies": { + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.8.0", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/postcss": "^4.3.0", + "@tanstack/react-query": "^5.100.10", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^17.4.2", + "ioredis": "^5.10.1", + "jose": "^6.2.3", + "lucide-react": "^1.16.0", + "next": "^16.2.6", + "pg": "^8.20.0", + "qrcode.react": "^4.2.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0", + "viem": "^2.49.3", + "wagmi": "^3.6.15", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^25.8.0", + "@types/pg": "^8.20.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "prisma": "^7.8.0", + "tsx": "^4.22.1", + "typescript": "^6.0.3" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", + "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", + "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@wagmi/connectors": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.0.14.tgz", + "integrity": "sha512-B+iMcT2wGBDujL8dX3g1vk0iZ94h0BK+S+6kQckItGWDkoNt7mterVIqFoYePtAqw3dmG4Y/W50cTxwplBXOjQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@base-org/account": "^2.5.1", + "@coinbase/wallet-sdk": "^4.3.6", + "@metamask/connect-evm": "^1.0.0", + "@safe-global/safe-apps-provider": "~0.18.6", + "@safe-global/safe-apps-sdk": "^9.1.0", + "@wagmi/core": "3.4.12", + "@walletconnect/ethereum-provider": "^2.21.1", + "accounts": "~0.10", + "porto": "~0.2.35", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@base-org/account": { + "optional": true + }, + "@coinbase/wallet-sdk": { + "optional": true + }, + "@metamask/connect-evm": { + "optional": true + }, + "@safe-global/safe-apps-provider": { + "optional": true + }, + "@safe-global/safe-apps-sdk": { + "optional": true + }, + "@walletconnect/ethereum-provider": { + "optional": true + }, + "accounts": { + "optional": true + }, + "porto": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/core": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.12.tgz", + "integrity": "sha512-q/NYoq+Up6JNfWNE6G0iXj9cHBSYgOyC8toqJLBWTeUnY1Ha9Q4OyFUHnXfvGTudbC0Gxhh7uUkqW3/LGdsQfg==", + "license": "MIT", + "dependencies": { + "eventemitter3": "5.0.1", + "mipd": "0.0.7", + "zustand": "5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.0.0", + "accounts": "~0.12", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "accounts": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", + "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.12.19", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.19.tgz", + "integrity": "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mipd": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", + "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "license": "MIT", + "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" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "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" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/ox": { + "version": "0.14.20", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz", + "integrity": "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "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" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.49.3", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.49.3.tgz", + "integrity": "sha512-FlIXd2kRygDxJtvjtPp74vjmyOKMjKlXXgTNdMxr8h3kcDrQ4bYb9q1MpSWyCVa3L2NJc9gSv+u8HcHYIZQUkw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.20", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wagmi": { + "version": "3.6.15", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.6.15.tgz", + "integrity": "sha512-/CmbLqS7VNiK3Rv9RPTcog1MSF/7oE1/QRVXHxlcv4oSHIMMpgUMunXtZx1MepV9Bmc0vGX9JvrPn6RAYjcvrA==", + "license": "MIT", + "dependencies": { + "@wagmi/connectors": "8.0.14", + "@wagmi/core": "3.4.12", + "use-sync-external-store": "1.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": ">=18", + "typescript": ">=5.7.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", + "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b1dd832 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "afriramp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "typecheck": "tsc --noEmit", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "prisma:seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.8.0", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/postcss": "^4.3.0", + "@tanstack/react-query": "^5.100.10", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^17.4.2", + "ioredis": "^5.10.1", + "jose": "^6.2.3", + "lucide-react": "^1.16.0", + "next": "^16.2.6", + "pg": "^8.20.0", + "qrcode.react": "^4.2.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0", + "viem": "^2.49.3", + "wagmi": "^3.6.15", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^25.8.0", + "@types/pg": "^8.20.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "prisma": "^7.8.0", + "tsx": "^4.22.1", + "typescript": "^6.0.3" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..5ed0260 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {} + } +}; + +export default config; diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..bbb4792 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +const databaseUrl = process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5432/afriramp?schema=public"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations" + }, + datasource: { + url: databaseUrl + } +}); diff --git a/prisma/migrations/20260518114700_initial/migration.sql b/prisma/migrations/20260518114700_initial/migration.sql new file mode 100644 index 0000000..882e2b2 --- /dev/null +++ b/prisma/migrations/20260518114700_initial/migration.sql @@ -0,0 +1,200 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "public"; + +-- CreateEnum +CREATE TYPE "Asset" AS ENUM ('USDT', 'USDC', 'WBTC', 'BTC'); + +-- CreateEnum +CREATE TYPE "Chain" AS ENUM ('POLYGON', 'BASE', 'BNB', 'BITCOIN'); + +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('BUY', 'SELL'); + +-- CreateEnum +CREATE TYPE "PaymentRail" AS ENUM ('MTN_MOMO', 'AIRTEL_MONEY'); + +-- CreateEnum +CREATE TYPE "TransactionStatus" AS ENUM ('QUOTE_CREATED', 'PAYMENT_PENDING', 'PAYMENT_CONFIRMED', 'CRYPTO_DISBURSEMENT_PENDING', 'CRYPTO_SENT', 'CRYPTO_DEPOSIT_PENDING', 'CRYPTO_DEPOSIT_CONFIRMED', 'PAYOUT_PENDING', 'PAYOUT_SENT', 'COMPLETED', 'FAILED', 'FROZEN', 'EXPIRED', 'MANUAL_REVIEW'); + +-- CreateEnum +CREATE TYPE "KycStatus" AS ENUM ('NOT_STARTED', 'PENDING', 'APPROVED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "WebhookStatus" AS ENUM ('RECEIVED', 'VERIFIED', 'REPLAYED', 'REJECTED', 'PROCESSED'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "walletAddress" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "kycStatus" "KycStatus" NOT NULL DEFAULT 'NOT_STARTED', + "riskScore" INTEGER NOT NULL DEFAULT 0, + "isBlocked" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Quote" ( + "id" TEXT NOT NULL, + "type" "TransactionType" NOT NULL, + "asset" "Asset" NOT NULL, + "chain" "Chain" NOT NULL, + "paymentRail" "PaymentRail" NOT NULL, + "fiatAmountZmw" DECIMAL(18,2) NOT NULL, + "cryptoAmount" DECIMAL(36,18) NOT NULL, + "feeZmw" DECIMAL(18,2) NOT NULL, + "rateZmw" DECIMAL(18,8) NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Quote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Transaction" ( + "id" TEXT NOT NULL, + "publicId" TEXT NOT NULL, + "type" "TransactionType" NOT NULL, + "status" "TransactionStatus" NOT NULL DEFAULT 'QUOTE_CREATED', + "asset" "Asset" NOT NULL, + "chain" "Chain" NOT NULL, + "paymentRail" "PaymentRail" NOT NULL, + "mobileNumber" TEXT NOT NULL, + "walletAddress" TEXT NOT NULL, + "depositAddress" TEXT, + "fiatAmountZmw" DECIMAL(18,2) NOT NULL, + "cryptoAmount" DECIMAL(36,18) NOT NULL, + "feeZmw" DECIMAL(18,2) NOT NULL, + "rateZmw" DECIMAL(18,8) NOT NULL, + "quoteExpiresAt" TIMESTAMP(3) NOT NULL, + "provider" TEXT NOT NULL, + "providerReference" TEXT, + "providerStatus" TEXT, + "blockchainTxHash" TEXT, + "payoutReference" TEXT, + "confirmations" INTEGER NOT NULL DEFAULT 0, + "idempotencyKey" TEXT NOT NULL, + "riskFlags" TEXT[] DEFAULT ARRAY[]::TEXT[], + "frozenReason" TEXT, + "failureReason" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "completedAt" TIMESTAMP(3), + "userId" TEXT, + "quoteId" TEXT, + CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WebhookEvent" ( + "id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "eventId" TEXT NOT NULL, + "signatureHash" TEXT NOT NULL, + "status" "WebhookStatus" NOT NULL DEFAULT 'RECEIVED', + "payload" JSONB NOT NULL, + "receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "processedAt" TIMESTAMP(3), + "transactionId" TEXT, + CONSTRAINT "WebhookEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IdempotencyKey" ( + "key" TEXT NOT NULL, + "route" TEXT NOT NULL, + "response" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "IdempotencyKey_pkey" PRIMARY KEY ("key") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" TEXT NOT NULL, + "actorType" TEXT NOT NULL, + "actorId" TEXT, + "action" TEXT NOT NULL, + "entityType" TEXT NOT NULL, + "entityId" TEXT NOT NULL, + "before" JSONB, + "after" JSONB, + "ipAddress" TEXT, + "userAgent" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT, + "transactionId" TEXT, + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AdminSession" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "AdminSession_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_walletAddress_key" ON "User"("walletAddress"); + +-- CreateIndex +CREATE INDEX "User_walletAddress_idx" ON "User"("walletAddress"); + +-- CreateIndex +CREATE INDEX "Quote_expiresAt_idx" ON "Quote"("expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "Transaction_publicId_key" ON "Transaction"("publicId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Transaction_providerReference_key" ON "Transaction"("providerReference"); + +-- CreateIndex +CREATE UNIQUE INDEX "Transaction_idempotencyKey_key" ON "Transaction"("idempotencyKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "Transaction_quoteId_key" ON "Transaction"("quoteId"); + +-- CreateIndex +CREATE INDEX "Transaction_walletAddress_idx" ON "Transaction"("walletAddress"); + +-- CreateIndex +CREATE INDEX "Transaction_mobileNumber_idx" ON "Transaction"("mobileNumber"); + +-- CreateIndex +CREATE INDEX "Transaction_status_idx" ON "Transaction"("status"); + +-- CreateIndex +CREATE INDEX "Transaction_createdAt_idx" ON "Transaction"("createdAt"); + +-- CreateIndex +CREATE INDEX "WebhookEvent_receivedAt_idx" ON "WebhookEvent"("receivedAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "WebhookEvent_provider_eventId_key" ON "WebhookEvent"("provider", "eventId"); + +-- CreateIndex +CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId"); + +-- CreateIndex +CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt"); + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_quoteId_fkey" FOREIGN KEY ("quoteId") REFERENCES "Quote"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WebhookEvent" ADD CONSTRAINT "WebhookEvent_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..133a9d8 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,191 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" +} + +enum Asset { + USDT + USDC + WBTC + BTC +} + +enum Chain { + POLYGON + BASE + BNB + BITCOIN +} + +enum TransactionType { + BUY + SELL +} + +enum PaymentRail { + MTN_MOMO + AIRTEL_MONEY +} + +enum TransactionStatus { + QUOTE_CREATED + PAYMENT_PENDING + PAYMENT_CONFIRMED + CRYPTO_DISBURSEMENT_PENDING + CRYPTO_SENT + CRYPTO_DEPOSIT_PENDING + CRYPTO_DEPOSIT_CONFIRMED + PAYOUT_PENDING + PAYOUT_SENT + COMPLETED + FAILED + FROZEN + EXPIRED + MANUAL_REVIEW +} + +enum KycStatus { + NOT_STARTED + PENDING + APPROVED + REJECTED +} + +enum WebhookStatus { + RECEIVED + VERIFIED + REPLAYED + REJECTED + PROCESSED +} + +model User { + id String @id @default(cuid()) + walletAddress String @unique + email String? + phone String? + kycStatus KycStatus @default(NOT_STARTED) + riskScore Int @default(0) + isBlocked Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + transactions Transaction[] + auditLogs AuditLog[] + + @@index([walletAddress]) +} + +model Quote { + id String @id @default(cuid()) + type TransactionType + asset Asset + chain Chain + paymentRail PaymentRail + fiatAmountZmw Decimal @db.Decimal(18, 2) + cryptoAmount Decimal @db.Decimal(36, 18) + feeZmw Decimal @db.Decimal(18, 2) + rateZmw Decimal @db.Decimal(18, 8) + expiresAt DateTime + createdAt DateTime @default(now()) + transaction Transaction? + + @@index([expiresAt]) +} + +model Transaction { + id String @id @default(cuid()) + publicId String @unique + type TransactionType + status TransactionStatus @default(QUOTE_CREATED) + asset Asset + chain Chain + paymentRail PaymentRail + mobileNumber String + walletAddress String + depositAddress String? + fiatAmountZmw Decimal @db.Decimal(18, 2) + cryptoAmount Decimal @db.Decimal(36, 18) + feeZmw Decimal @db.Decimal(18, 2) + rateZmw Decimal @db.Decimal(18, 8) + quoteExpiresAt DateTime + provider String + providerReference String? @unique + providerStatus String? + blockchainTxHash String? + payoutReference String? + confirmations Int @default(0) + idempotencyKey String @unique + riskFlags String[] @default([]) + frozenReason String? + failureReason String? + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + userId String? + user User? @relation(fields: [userId], references: [id]) + quoteId String? @unique + quote Quote? @relation(fields: [quoteId], references: [id]) + auditLogs AuditLog[] + webhooks WebhookEvent[] + + @@index([walletAddress]) + @@index([mobileNumber]) + @@index([status]) + @@index([createdAt]) +} + +model WebhookEvent { + id String @id @default(cuid()) + provider String + eventId String + signatureHash String + status WebhookStatus @default(RECEIVED) + payload Json + receivedAt DateTime @default(now()) + processedAt DateTime? + transactionId String? + transaction Transaction? @relation(fields: [transactionId], references: [id]) + + @@unique([provider, eventId]) + @@index([receivedAt]) +} + +model IdempotencyKey { + key String @id + route String + response Json? + createdAt DateTime @default(now()) + expiresAt DateTime +} + +model AuditLog { + id String @id @default(cuid()) + actorType String + actorId String? + action String + entityType String + entityId String + before Json? + after Json? + ipAddress String? + userAgent String? + createdAt DateTime @default(now()) + userId String? + user User? @relation(fields: [userId], references: [id]) + transactionId String? + transaction Transaction? @relation(fields: [transactionId], references: [id]) + + @@index([entityType, entityId]) + @@index([createdAt]) +} + +model AdminSession { + id String @id @default(cuid()) + email String + expiresAt DateTime + createdAt DateTime @default(now()) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..902dcf8 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,26 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +async function main() { + await prisma.auditLog.create({ + data: { + actorType: "system", + action: "database.seeded", + entityType: "system", + entityId: "bootstrap", + after: { + message: "AfriRamp database initialized. Configure admin credentials in environment variables." + } + } + }); +} + +main() + .catch((error) => { + console.error(error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/public/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..0453842 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,5 @@ +import { AdminDashboard } from "@/components/admin-dashboard"; + +export default function AdminPage() { + return ; +} diff --git a/src/app/api/admin/analytics/route.ts b/src/app/api/admin/analytics/route.ts new file mode 100644 index 0000000..e871a70 --- /dev/null +++ b/src/app/api/admin/analytics/route.ts @@ -0,0 +1,28 @@ +import { fail, handleApiError, ok } from "@/lib/api"; +import { requireAdmin } from "@/lib/admin-auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + try { + await requireAdmin(); + const [transactions, completed, manualReview] = await Promise.all([ + prisma.transaction.findMany({ select: { feeZmw: true, fiatAmountZmw: true, status: true, type: true } }), + prisma.transaction.count({ where: { status: "COMPLETED" } }), + prisma.transaction.count({ where: { status: "MANUAL_REVIEW" } }) + ]); + + const totalFeesZmw = transactions.reduce((sum, tx) => sum + Number(tx.feeZmw), 0); + const volumeZmw = transactions.reduce((sum, tx) => sum + Number(tx.fiatAmountZmw), 0); + + return ok({ + totalTransactions: transactions.length, + completed, + manualReview, + volumeZmw, + totalFeesZmw + }); + } catch (error) { + if (error instanceof Error && error.message.includes("Admin authentication")) return fail("Admin authentication required.", 401, "UNAUTHORIZED"); + return handleApiError(error); + } +} diff --git a/src/app/api/admin/liquidity/route.ts b/src/app/api/admin/liquidity/route.ts new file mode 100644 index 0000000..2c46836 --- /dev/null +++ b/src/app/api/admin/liquidity/route.ts @@ -0,0 +1,25 @@ +import { fail, handleApiError, ok } from "@/lib/api"; +import { requireAdmin } from "@/lib/admin-auth"; +import { getHotWalletBalance } from "@/lib/blockchain/hot-wallet"; + +export async function GET() { + try { + await requireAdmin(); + const [usdcBase, usdtPolygon, wbtcPolygon] = await Promise.all([ + getHotWalletBalance({ asset: "USDC", chain: "BASE" }), + getHotWalletBalance({ asset: "USDT", chain: "POLYGON" }), + getHotWalletBalance({ asset: "WBTC", chain: "POLYGON" }) + ]); + + return ok({ + balances: [ + { asset: "USDC", chain: "BASE", raw: usdcBase }, + { asset: "USDT", chain: "POLYGON", raw: usdtPolygon }, + { asset: "WBTC", chain: "POLYGON", raw: wbtcPolygon } + ] + }); + } catch (error) { + if (error instanceof Error && error.message.includes("Admin authentication")) return fail("Admin authentication required.", 401, "UNAUTHORIZED"); + return handleApiError(error); + } +} diff --git a/src/app/api/admin/login/route.ts b/src/app/api/admin/login/route.ts new file mode 100644 index 0000000..57e5dd7 --- /dev/null +++ b/src/app/api/admin/login/route.ts @@ -0,0 +1,17 @@ +import { fail, handleApiError, ok, readJson } from "@/lib/api"; +import { createAdminToken, setAdminCookie, verifyAdminPassword } from "@/lib/admin-auth"; +import { adminLoginSchema } from "@/lib/validators"; + +export async function POST(request: Request) { + try { + const body = adminLoginSchema.parse(await readJson(request)); + const valid = await verifyAdminPassword(body.email, body.password); + if (!valid) return fail("Invalid admin credentials.", 401, "UNAUTHORIZED"); + + const token = await createAdminToken(body.email); + await setAdminCookie(token); + return ok({ authenticated: true }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/admin/transactions/[publicId]/route.ts b/src/app/api/admin/transactions/[publicId]/route.ts new file mode 100644 index 0000000..26d5865 --- /dev/null +++ b/src/app/api/admin/transactions/[publicId]/route.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +import { auditLog } from "@/lib/audit"; +import { fail, handleApiError, ok, readJson } from "@/lib/api"; +import { requireAdmin } from "@/lib/admin-auth"; +import { prisma } from "@/lib/prisma"; + +const patchSchema = z.object({ + action: z.enum(["freeze", "manual_review", "fail"]), + reason: z.string().min(3).max(500) +}); + +export async function PATCH(request: Request, { params }: { params: Promise<{ publicId: string }> }) { + try { + const admin = await requireAdmin(); + const { publicId } = await params; + const body = patchSchema.parse(await readJson(request)); + const transaction = await prisma.transaction.findUnique({ where: { publicId } }); + if (!transaction) return fail("Transaction not found.", 404, "TRANSACTION_NOT_FOUND"); + + const status = body.action === "freeze" ? "FROZEN" : body.action === "fail" ? "FAILED" : "MANUAL_REVIEW"; + const updated = await prisma.transaction.update({ + where: { id: transaction.id }, + data: { + status, + frozenReason: body.action === "freeze" ? body.reason : transaction.frozenReason, + failureReason: body.action === "fail" ? body.reason : transaction.failureReason + } + }); + + await auditLog({ + actorType: "admin", + actorId: admin.email, + action: `transaction.${body.action}`, + entityType: "transaction", + entityId: updated.publicId, + before: { status: transaction.status }, + after: { status: updated.status, reason: body.reason }, + transactionId: updated.id + }); + + return ok({ publicId: updated.publicId, status: updated.status }); + } catch (error) { + if (error instanceof Error && error.message.includes("Admin authentication")) return fail("Admin authentication required.", 401, "UNAUTHORIZED"); + return handleApiError(error); + } +} diff --git a/src/app/api/admin/transactions/route.ts b/src/app/api/admin/transactions/route.ts new file mode 100644 index 0000000..ab462dc --- /dev/null +++ b/src/app/api/admin/transactions/route.ts @@ -0,0 +1,36 @@ +import { fail, handleApiError, ok } from "@/lib/api"; +import { requireAdmin } from "@/lib/admin-auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET(request: Request) { + try { + await requireAdmin(); + const url = new URL(request.url); + const status = url.searchParams.get("status") ?? undefined; + const transactions = await prisma.transaction.findMany({ + where: status ? { status: status as never } : undefined, + orderBy: { createdAt: "desc" }, + take: 100 + }); + + return ok({ + transactions: transactions.map((transaction) => ({ + publicId: transaction.publicId, + type: transaction.type, + status: transaction.status, + asset: transaction.asset, + paymentRail: transaction.paymentRail, + mobileNumber: transaction.mobileNumber, + walletAddress: transaction.walletAddress, + fiatAmountZmw: transaction.fiatAmountZmw.toString(), + feeZmw: transaction.feeZmw.toString(), + riskFlags: transaction.riskFlags, + frozenReason: transaction.frozenReason, + createdAt: transaction.createdAt.toISOString() + })) + }); + } catch (error) { + if (error instanceof Error && error.message.includes("Admin authentication")) return fail("Admin authentication required.", 401, "UNAUTHORIZED"); + return handleApiError(error); + } +} diff --git a/src/app/api/buy/route.ts b/src/app/api/buy/route.ts new file mode 100644 index 0000000..64109b0 --- /dev/null +++ b/src/app/api/buy/route.ts @@ -0,0 +1,99 @@ +import { Prisma, TransactionType } from "@prisma/client"; + +import { auditLog } from "@/lib/audit"; +import { fail, handleApiError, ok, readJson } from "@/lib/api"; +import { getServerEnv } from "@/lib/env"; +import { reserveIdempotencyKey, storeIdempotentResponse } from "@/lib/idempotency"; +import { getMobileMoneyProvider } from "@/lib/payments"; +import { prisma } from "@/lib/prisma"; +import { assessBasicRisk } from "@/lib/risk"; +import { createPublicId, normalizePhone, normalizeWallet } from "@/lib/security"; +import { nextInitialStatus } from "@/lib/state-machine"; +import { buyRequestSchema } from "@/lib/validators"; + +export async function POST(request: Request) { + try { + const body = buyRequestSchema.parse(await readJson(request)); + const reserved = await reserveIdempotencyKey({ key: body.idempotencyKey, route: "POST /api/buy" }); + if (reserved.reused && reserved.response) return ok(reserved.response); + + const quote = await prisma.quote.findUnique({ where: { id: body.quoteId } }); + if (!quote || quote.type !== TransactionType.BUY) return fail("Quote not found.", 404, "QUOTE_NOT_FOUND"); + if (quote.expiresAt < new Date()) return fail("Quote expired. Please request a new rate.", 410, "QUOTE_EXPIRED"); + + const mobileNumber = normalizePhone(body.mobileNumber); + const walletAddress = normalizeWallet(body.walletAddress); + const publicId = createPublicId("BUY"); + const provider = getMobileMoneyProvider(); + const riskFlags = assessBasicRisk({ + fiatAmountZmw: Number(quote.fiatAmountZmw), + mobileNumber, + walletAddress, + paymentRail: quote.paymentRail + }); + + const user = await prisma.user.upsert({ + where: { walletAddress }, + update: { phone: mobileNumber }, + create: { walletAddress, phone: mobileNumber } + }); + + const collection = await provider.initiateCollection({ + amountZmw: Number(quote.fiatAmountZmw), + mobileNumber, + rail: quote.paymentRail, + reference: publicId, + callbackUrl: getServerEnv().MOBILE_MONEY_CALLBACK_URL || undefined, + description: `AfriRamp ${quote.asset} purchase ${publicId}` + }); + + const transaction = await prisma.transaction.create({ + data: { + publicId, + type: "BUY", + status: riskFlags.length ? "MANUAL_REVIEW" : nextInitialStatus("BUY"), + asset: quote.asset, + chain: quote.chain, + paymentRail: quote.paymentRail, + mobileNumber, + walletAddress, + fiatAmountZmw: quote.fiatAmountZmw, + cryptoAmount: quote.cryptoAmount, + feeZmw: quote.feeZmw, + rateZmw: quote.rateZmw, + quoteExpiresAt: quote.expiresAt, + provider: collection.provider, + providerReference: collection.reference, + providerStatus: collection.status, + idempotencyKey: body.idempotencyKey, + riskFlags, + metadata: { providerRaw: collection.raw } as Prisma.InputJsonValue, + quoteId: quote.id, + userId: user.id + } + }); + + await auditLog({ + actorType: "user", + actorId: walletAddress, + action: "transaction.buy.created", + entityType: "transaction", + entityId: transaction.publicId, + after: { status: transaction.status, riskFlags }, + userId: user.id, + transactionId: transaction.id + }); + + const response = { + publicId: transaction.publicId, + status: transaction.status, + provider: transaction.provider, + providerReference: transaction.providerReference, + nextStep: riskFlags.length ? "Manual review required before payment is accepted." : "Approve the mobile money prompt on your phone." + }; + await storeIdempotentResponse(body.idempotencyKey, response); + return ok(response, { status: 201 }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..b5d80ab --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { ok } from "@/lib/api"; + +export function GET() { + return ok({ status: "healthy", timestamp: new Date().toISOString() }); +} diff --git a/src/app/api/quote/route.ts b/src/app/api/quote/route.ts new file mode 100644 index 0000000..f4ca73a --- /dev/null +++ b/src/app/api/quote/route.ts @@ -0,0 +1,60 @@ +import { Prisma } from "@prisma/client"; + +import { fail, handleApiError, ok, readJson } from "@/lib/api"; +import { ASSETS } from "@/lib/constants"; +import { getServerEnv } from "@/lib/env"; +import { prisma } from "@/lib/prisma"; +import { createQuoteCalculation } from "@/lib/rates"; +import { quoteRequestSchema } from "@/lib/validators"; + +export async function POST(request: Request) { + try { + const body = quoteRequestSchema.parse(await readJson(request)); + if (body.asset === "BTC") { + return fail("Native Bitcoin is planned for a later phase. Use WBTC on Polygon for the MVP.", 422, "ASSET_NOT_AVAILABLE"); + } + + const asset = ASSETS.find((item) => item.value === body.asset); + if (!asset || asset.defaultChain !== body.chain) { + return fail(`${body.asset} is currently supported on ${asset?.defaultChain ?? "a configured low-fee chain"}.`, 422, "CHAIN_NOT_SUPPORTED"); + } + + const env = getServerEnv(); + const calculation = await createQuoteCalculation(body); + if (calculation.fiatAmountZmw <= 0 || calculation.cryptoAmount <= 0) { + return fail("Enter a positive amount to quote.", 422, "INVALID_AMOUNT"); + } + if (calculation.fiatAmountZmw > env.MAX_SINGLE_TRANSACTION_ZMW) { + return fail("This amount needs manual review. Please contact support.", 422, "LIMIT_EXCEEDED"); + } + + const quote = await prisma.quote.create({ + data: { + type: body.type, + asset: body.asset, + chain: body.chain, + paymentRail: body.paymentRail, + fiatAmountZmw: new Prisma.Decimal(calculation.fiatAmountZmw.toFixed(2)), + cryptoAmount: new Prisma.Decimal(calculation.cryptoAmount.toFixed(18)), + feeZmw: new Prisma.Decimal(calculation.feeZmw.toFixed(2)), + rateZmw: new Prisma.Decimal(calculation.rateZmw.toFixed(8)), + expiresAt: new Date(Date.now() + env.QUOTE_LOCK_SECONDS * 1000) + } + }); + + return ok({ + id: quote.id, + type: quote.type, + asset: quote.asset, + chain: quote.chain, + paymentRail: quote.paymentRail, + fiatAmountZmw: Number(quote.fiatAmountZmw), + cryptoAmount: Number(quote.cryptoAmount), + feeZmw: Number(quote.feeZmw), + rateZmw: Number(quote.rateZmw), + expiresAt: quote.expiresAt.toISOString() + }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/sell/route.ts b/src/app/api/sell/route.ts new file mode 100644 index 0000000..deae023 --- /dev/null +++ b/src/app/api/sell/route.ts @@ -0,0 +1,91 @@ +import { TransactionType } from "@prisma/client"; + +import { auditLog } from "@/lib/audit"; +import { fail, handleApiError, ok, readJson } from "@/lib/api"; +import { allocateDepositAddress } from "@/lib/blockchain/hot-wallet"; +import { reserveIdempotencyKey, storeIdempotentResponse } from "@/lib/idempotency"; +import { prisma } from "@/lib/prisma"; +import { assessBasicRisk } from "@/lib/risk"; +import { createPublicId, normalizePhone, normalizeWallet } from "@/lib/security"; +import { nextInitialStatus } from "@/lib/state-machine"; +import { sellRequestSchema } from "@/lib/validators"; + +export async function POST(request: Request) { + try { + const body = sellRequestSchema.parse(await readJson(request)); + const reserved = await reserveIdempotencyKey({ key: body.idempotencyKey, route: "POST /api/sell" }); + if (reserved.reused && reserved.response) return ok(reserved.response); + + const quote = await prisma.quote.findUnique({ where: { id: body.quoteId } }); + if (!quote || quote.type !== TransactionType.SELL) return fail("Quote not found.", 404, "QUOTE_NOT_FOUND"); + if (quote.expiresAt < new Date()) return fail("Quote expired. Please request a new rate.", 410, "QUOTE_EXPIRED"); + + const depositAddress = allocateDepositAddress(); + if (!depositAddress) return fail("Deposit wallet is not configured. Contact support.", 503, "DEPOSIT_WALLET_NOT_CONFIGURED"); + + const mobileNumber = normalizePhone(body.mobileNumber); + const walletAddress = normalizeWallet(body.walletAddress); + const publicId = createPublicId("SELL"); + const riskFlags = assessBasicRisk({ + fiatAmountZmw: Number(quote.fiatAmountZmw), + mobileNumber, + walletAddress, + paymentRail: quote.paymentRail + }); + + const user = await prisma.user.upsert({ + where: { walletAddress }, + update: { phone: mobileNumber }, + create: { walletAddress, phone: mobileNumber } + }); + + const transaction = await prisma.transaction.create({ + data: { + publicId, + type: "SELL", + status: riskFlags.length ? "MANUAL_REVIEW" : nextInitialStatus("SELL"), + asset: quote.asset, + chain: quote.chain, + paymentRail: quote.paymentRail, + mobileNumber, + walletAddress, + depositAddress, + fiatAmountZmw: quote.fiatAmountZmw, + cryptoAmount: quote.cryptoAmount, + feeZmw: quote.feeZmw, + rateZmw: quote.rateZmw, + quoteExpiresAt: quote.expiresAt, + provider: process.env.MOBILE_MONEY_PROVIDER ?? "mock", + idempotencyKey: body.idempotencyKey, + riskFlags, + quoteId: quote.id, + userId: user.id + } + }); + + await auditLog({ + actorType: "user", + actorId: walletAddress, + action: "transaction.sell.created", + entityType: "transaction", + entityId: transaction.publicId, + after: { status: transaction.status, depositAddress, riskFlags }, + userId: user.id, + transactionId: transaction.id + }); + + const response = { + publicId: transaction.publicId, + status: transaction.status, + depositAddress, + asset: transaction.asset, + chain: transaction.chain, + amount: transaction.cryptoAmount.toString(), + nextStep: "Send the exact crypto amount from your connected wallet. The backend will verify confirmations before payout." + }; + await storeIdempotentResponse(body.idempotencyKey, response); + return ok(response, { status: 201 }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/transactions/[publicId]/deposit/route.ts b/src/app/api/transactions/[publicId]/deposit/route.ts new file mode 100644 index 0000000..0bbf3ea --- /dev/null +++ b/src/app/api/transactions/[publicId]/deposit/route.ts @@ -0,0 +1,91 @@ +import { z } from "zod"; +import { Prisma } from "@prisma/client"; + +import { auditLog } from "@/lib/audit"; +import { fail, handleApiError, ok, readJson } from "@/lib/api"; +import { verifyTokenTransfer } from "@/lib/blockchain/verify"; +import { getServerEnv } from "@/lib/env"; +import { getMobileMoneyProvider } from "@/lib/payments"; +import { prisma } from "@/lib/prisma"; + +const depositSchema = z.object({ + txHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/) +}); + +export async function POST(request: Request, { params }: { params: Promise<{ publicId: string }> }) { + try { + const { publicId } = await params; + const { txHash } = depositSchema.parse(await readJson(request)); + const env = getServerEnv(); + const transaction = await prisma.transaction.findUnique({ where: { publicId } }); + if (!transaction || transaction.type !== "SELL") return fail("Sell transaction not found.", 404, "TRANSACTION_NOT_FOUND"); + if (!transaction.depositAddress) return fail("Deposit address missing.", 409, "DEPOSIT_ADDRESS_MISSING"); + if (transaction.asset === "BTC" || transaction.chain === "BITCOIN") { + return fail("Native Bitcoin verification is not enabled for the MVP.", 422, "BTC_NOT_ENABLED"); + } + + const verification = await verifyTokenTransfer({ + chain: transaction.chain, + asset: transaction.asset, + txHash: txHash as `0x${string}`, + expectedFrom: transaction.walletAddress as `0x${string}`, + expectedTo: transaction.depositAddress as `0x${string}`, + expectedAmount: transaction.cryptoAmount.toString() + }); + + if (!verification.valid) return fail("Deposit transaction does not match expected sender, receiver, asset, or amount.", 422, "INVALID_DEPOSIT"); + if (verification.confirmations < env.REQUIRED_EVM_CONFIRMATIONS) { + await prisma.transaction.update({ + where: { id: transaction.id }, + data: { blockchainTxHash: txHash, confirmations: verification.confirmations } + }); + return ok({ status: "CRYPTO_DEPOSIT_PENDING", confirmations: verification.confirmations, required: env.REQUIRED_EVM_CONFIRMATIONS }); + } + + const locked = await prisma.transaction.updateMany({ + where: { id: transaction.id, status: "CRYPTO_DEPOSIT_PENDING" }, + data: { + status: "PAYOUT_PENDING", + blockchainTxHash: txHash, + confirmations: verification.confirmations + } + }); + if (locked.count === 0) return ok({ status: transaction.status, alreadyProcessed: true }); + + const provider = getMobileMoneyProvider(); + const payout = await provider.initiatePayout({ + amountZmw: Number(transaction.fiatAmountZmw), + mobileNumber: transaction.mobileNumber, + rail: transaction.paymentRail, + reference: transaction.publicId, + description: `AfriRamp ${transaction.asset} sale payout ${transaction.publicId}` + }); + + const updated = await prisma.transaction.update({ + where: { id: transaction.id }, + data: { + payoutReference: payout.reference, + providerStatus: payout.status, + metadata: { payoutRaw: payout.raw } as Prisma.InputJsonValue + } + }); + + await auditLog({ + actorType: "system", + action: "transaction.sell.deposit_confirmed", + entityType: "transaction", + entityId: updated.publicId, + after: { txHash, confirmations: verification.confirmations, payoutReference: payout.reference }, + transactionId: updated.id + }); + + return ok({ + publicId: updated.publicId, + status: updated.status, + payoutReference: updated.payoutReference, + confirmations: updated.confirmations + }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/transactions/[publicId]/route.ts b/src/app/api/transactions/[publicId]/route.ts new file mode 100644 index 0000000..82d1d11 --- /dev/null +++ b/src/app/api/transactions/[publicId]/route.ts @@ -0,0 +1,32 @@ +import { fail, handleApiError, ok } from "@/lib/api"; +import { prisma } from "@/lib/prisma"; + +export async function GET(_request: Request, { params }: { params: Promise<{ publicId: string }> }) { + try { + const { publicId } = await params; + const transaction = await prisma.transaction.findUnique({ where: { publicId } }); + if (!transaction) return fail("Transaction not found.", 404, "TRANSACTION_NOT_FOUND"); + + return ok({ + publicId: transaction.publicId, + type: transaction.type, + status: transaction.status, + asset: transaction.asset, + chain: transaction.chain, + paymentRail: transaction.paymentRail, + fiatAmountZmw: transaction.fiatAmountZmw.toString(), + cryptoAmount: transaction.cryptoAmount.toString(), + feeZmw: transaction.feeZmw.toString(), + providerReference: transaction.providerReference, + payoutReference: transaction.payoutReference, + blockchainTxHash: transaction.blockchainTxHash, + depositAddress: transaction.depositAddress, + confirmations: transaction.confirmations, + riskFlags: transaction.riskFlags, + createdAt: transaction.createdAt.toISOString(), + completedAt: transaction.completedAt?.toISOString() ?? null + }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/transactions/route.ts b/src/app/api/transactions/route.ts new file mode 100644 index 0000000..ae9115d --- /dev/null +++ b/src/app/api/transactions/route.ts @@ -0,0 +1,43 @@ +import { handleApiError, ok } from "@/lib/api"; +import { prisma } from "@/lib/prisma"; +import { normalizeWallet } from "@/lib/security"; + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const walletAddress = url.searchParams.get("walletAddress"); + const publicId = url.searchParams.get("publicId"); + + const transactions = await prisma.transaction.findMany({ + where: { + ...(walletAddress ? { walletAddress: normalizeWallet(walletAddress) } : {}), + ...(publicId ? { publicId } : {}) + }, + orderBy: { createdAt: "desc" }, + take: 50 + }); + + return ok({ + transactions: transactions.map((transaction) => ({ + publicId: transaction.publicId, + type: transaction.type, + status: transaction.status, + asset: transaction.asset, + chain: transaction.chain, + paymentRail: transaction.paymentRail, + walletAddress: transaction.walletAddress, + fiatAmountZmw: transaction.fiatAmountZmw.toString(), + cryptoAmount: transaction.cryptoAmount.toString(), + feeZmw: transaction.feeZmw.toString(), + providerReference: transaction.providerReference, + payoutReference: transaction.payoutReference, + blockchainTxHash: transaction.blockchainTxHash, + depositAddress: transaction.depositAddress, + createdAt: transaction.createdAt.toISOString(), + completedAt: transaction.completedAt?.toISOString() ?? null + })) + }); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/app/api/webhooks/mobile-money/[provider]/route.ts b/src/app/api/webhooks/mobile-money/[provider]/route.ts new file mode 100644 index 0000000..46e0c71 --- /dev/null +++ b/src/app/api/webhooks/mobile-money/[provider]/route.ts @@ -0,0 +1,126 @@ +import { WebhookStatus } from "@prisma/client"; + +import { auditLog } from "@/lib/audit"; +import { handleApiError, ok } from "@/lib/api"; +import { sendTokenFromHotWallet } from "@/lib/blockchain/hot-wallet"; +import { enqueueJob } from "@/lib/queue"; +import { getMobileMoneyProvider } from "@/lib/payments"; +import { prisma } from "@/lib/prisma"; +import { hashValue } from "@/lib/security"; + +export async function POST(request: Request, { params }: { params: Promise<{ provider: string }> }) { + try { + const { provider } = await params; + const rawBody = await request.text(); + const paymentProvider = getMobileMoneyProvider(); + if (paymentProvider.name !== provider) { + return ok({ ignored: true, reason: "Provider route does not match configured provider." }, { status: 202 }); + } + + const webhook = await paymentProvider.parseWebhook(rawBody, request.headers); + const signatureHash = hashValue(request.headers.get("x-signature") ?? request.headers.get("x-afriramp-signature") ?? rawBody); + + const existing = await prisma.webhookEvent.findUnique({ + where: { provider_eventId: { provider, eventId: webhook.eventId } } + }); + if (existing) { + await prisma.webhookEvent.update({ where: { id: existing.id }, data: { status: WebhookStatus.REPLAYED } }); + return ok({ replayed: true }); + } + + const transaction = await prisma.transaction.findFirst({ + where: { + OR: [{ providerReference: webhook.reference }, { publicId: webhook.reference }, { payoutReference: webhook.reference }] + } + }); + + const webhookEvent = await prisma.webhookEvent.create({ + data: { + provider, + eventId: webhook.eventId, + signatureHash, + status: WebhookStatus.VERIFIED, + payload: webhook.raw as never, + transactionId: transaction?.id + } + }); + + if (!transaction) { + return ok({ accepted: true, warning: "No matching transaction." }, { status: 202 }); + } + + if (webhook.status === "failed") { + await prisma.transaction.update({ + where: { id: transaction.id }, + data: { status: "FAILED", providerStatus: "failed", failureReason: "Mobile money provider reported failure." } + }); + await markWebhookProcessed(webhookEvent.id); + return ok({ accepted: true, status: "FAILED" }); + } + + if (transaction.type === "BUY" && webhook.status === "successful") { + const locked = await prisma.transaction.updateMany({ + where: { id: transaction.id, status: "PAYMENT_PENDING", blockchainTxHash: null }, + data: { status: "CRYPTO_DISBURSEMENT_PENDING", providerStatus: "successful" } + }); + if (locked.count === 0) { + await markWebhookProcessed(webhookEvent.id); + return ok({ accepted: true, alreadyProcessed: true }); + } + + try { + const sent = await sendTokenFromHotWallet({ + asset: transaction.asset, + chain: transaction.chain, + to: transaction.walletAddress as `0x${string}`, + amount: transaction.cryptoAmount.toString(), + reference: transaction.publicId + }); + + await prisma.transaction.update({ + where: { id: transaction.id }, + data: { + status: "COMPLETED", + blockchainTxHash: sent.hash, + completedAt: new Date(), + metadata: { simulatedBroadcast: sent.simulated } + } + }); + await auditLog({ + actorType: "webhook", + actorId: provider, + action: "transaction.buy.completed", + entityType: "transaction", + entityId: transaction.publicId, + after: { blockchainTxHash: sent.hash, simulatedBroadcast: sent.simulated }, + transactionId: transaction.id + }); + await enqueueJob("notifications", { transactionId: transaction.id, type: "receipt" }); + } catch (error) { + await prisma.transaction.update({ + where: { id: transaction.id }, + data: { status: "MANUAL_REVIEW", failureReason: error instanceof Error ? error.message : "Crypto disbursement failed." } + }); + } + } + + if (transaction.type === "SELL" && webhook.status === "successful" && transaction.status === "PAYOUT_PENDING") { + await prisma.transaction.update({ + where: { id: transaction.id }, + data: { status: "COMPLETED", providerStatus: "successful", completedAt: new Date() } + }); + } + + await markWebhookProcessed(webhookEvent.id); + return ok({ accepted: true }); + } catch (error) { + return handleApiError(error); + } +} + +async function markWebhookProcessed(id: string) { + await prisma.webhookEvent.update({ + where: { id }, + data: { status: WebhookStatus.PROCESSED, processedAt: new Date() } + }); +} diff --git a/src/app/buy/page.tsx b/src/app/buy/page.tsx new file mode 100644 index 0000000..2bd2ffb --- /dev/null +++ b/src/app/buy/page.tsx @@ -0,0 +1,5 @@ +import { RampForm } from "@/components/ramp-form"; + +export default function BuyPage() { + return ; +} diff --git a/src/app/faq/page.tsx b/src/app/faq/page.tsx new file mode 100644 index 0000000..96eb5ff --- /dev/null +++ b/src/app/faq/page.tsx @@ -0,0 +1,29 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +const faqs = [ + ["Is AfriRamp custodial?", "No. Users connect MetaMask and receive/send crypto from their own wallet. The platform only uses operational hot-wallet liquidity while a transaction is being fulfilled."], + ["Why WBTC instead of native Bitcoin first?", "For MVP simplicity, WBTC on Polygon keeps the same EVM wallet flow, lower fees, and simpler transaction verification. Native Bitcoin can be added after deposit-address, confirmations, and treasury controls mature."], + ["What is the fee?", "A fixed 3% platform fee is included in every quote before the user confirms."], + ["Which mobile money networks are supported?", "The product is designed around MTN Mobile Money and Airtel Money in Zambia, with a provider abstraction for Lipila, Flutterwave, PayChangu, or Pesapal."] +]; + +export default function FaqPage() { + return ( +
+ + + Frequently asked questions + Plain-language answers for first-time crypto users in Zambia. + + + {faqs.map(([question, answer]) => ( +
+

{question}

+

{answer}

+
+ ))} +
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..03775a7 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,74 @@ +@import "tailwindcss"; + +:root { + --background: #f7fbf7; + --foreground: #102016; + --card: #ffffff; + --card-foreground: #102016; + --muted: #e9f2eb; + --muted-foreground: #58705f; + --primary: #0f9f6e; + --primary-foreground: #ffffff; + --secondary: #ffe7a3; + --secondary-foreground: #2b2105; + --destructive: #d64545; + --border: #dbe7df; + --ring: #0f9f6e; + color-scheme: light; +} + +.dark { + --background: #08130d; + --foreground: #effaf2; + --card: #0d1d13; + --card-foreground: #effaf2; + --muted: #14281b; + --muted-foreground: #9db0a3; + --primary: #2bd68f; + --primary-foreground: #062112; + --secondary: #f8c84b; + --secondary-foreground: #1d1602; + --destructive: #ff6b6b; + --border: #24402d; + --ring: #2bd68f; + color-scheme: dark; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-ring: var(--ring); + --font-sans: var(--font-inter); +} + +* { + border-color: var(--border); +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(43, 214, 143, 0.16), transparent 32rem), + radial-gradient(circle at top right, rgba(248, 200, 75, 0.16), transparent 28rem), + var(--background); + color: var(--foreground); + font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif; +} + +button, +input, +select, +textarea { + font: inherit; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..0e0f31c --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; + +import { MobileNav } from "@/components/mobile-nav"; +import { Providers } from "@/components/providers"; +import { SiteHeader } from "@/components/site-header"; +import "./globals.css"; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], + display: "swap" +}); + +export const metadata: Metadata = { + title: "AfriRamp | Zambia mobile money crypto ramp", + description: "A non-custodial crypto on-ramp and off-ramp MVP for Zambia using MTN and Airtel Mobile Money.", + metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000") +}; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + + {children} + + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..b59866d --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,89 @@ +import Link from "next/link"; +import { ArrowRight, Banknote, CheckCircle2, LockKeyhole, ShieldCheck, Smartphone, Wallet } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +const features = [ + { icon: Smartphone, title: "Pay with mobile money", text: "MTN Mobile Money and Airtel Money rails with a swappable provider layer." }, + { icon: Wallet, title: "Use your own wallet", text: "MetaMask connects directly. Users never keep balances inside AfriRamp accounts." }, + { icon: LockKeyhole, title: "Locked quotes", text: "Short-lived rates show exact crypto, ZMW payout, and the fixed 3% fee upfront." }, + { icon: ShieldCheck, title: "Built for controls", text: "Webhook replay checks, idempotency, manual review, frozen states, and full audit logs." } +]; + +export default function LandingPage() { + return ( +
+
+
+ Zambia MVP • Non-custodial +

+ Buy and sell crypto with mobile money, without surrendering your wallet. +

+

+ AfriRamp is a lean on-ramp/off-ramp for Zambia focused on USDT, USDC, and BTC exposure through low-fee EVM chains. +

+
+ + +
+
+ 3% transparent fee + MTN + Airtel + MetaMask ready +
+
+ + + + Simple ramp receipt + A beginner-friendly confirmation before money moves. + + + {[ + ["You pay", "ZMW 1,000.00"], + ["Platform fee", "ZMW 30.00"], + ["You receive", "≈ 35.92 USDC"], + ["Destination", "0x7a...91c2"], + ["Status", "Awaiting mobile money"] + ].map(([label, value]) => ( +
+ {label} + {value} +
+ ))} +
+ +

MVP recommendation

+

+ Launch USDC on Base and USDT/WBTC on Polygon first. Add native Bitcoin when operational controls mature. +

+
+
+
+
+ +
+ {features.map((feature) => { + const Icon = feature.icon; + return ( + + + + {feature.title} + {feature.text} + + + ); + })} +
+
+ ); +} diff --git a/src/app/sell/page.tsx b/src/app/sell/page.tsx new file mode 100644 index 0000000..fb4ce6f --- /dev/null +++ b/src/app/sell/page.tsx @@ -0,0 +1,5 @@ +import { RampForm } from "@/components/ramp-form"; + +export default function SellPage() { + return ; +} diff --git a/src/app/support/page.tsx b/src/app/support/page.tsx new file mode 100644 index 0000000..a749d28 --- /dev/null +++ b/src/app/support/page.tsx @@ -0,0 +1,32 @@ +import { Mail, MessageCircle, ShieldAlert } from "lucide-react"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function SupportPage() { + return ( +
+ + + Support + Keep support simple and human for the MVP. + + + {[ + { icon: MessageCircle, title: "WhatsApp support", text: "Add a verified WhatsApp Business number before launch." }, + { icon: Mail, title: "Email receipts", text: "Send quote, payment, and completion receipts from API events." }, + { icon: ShieldAlert, title: "Fraud review", text: "Freeze suspicious transactions and ask for extra verification." } + ].map((item) => { + const Icon = item.icon; + return ( +
+ +

{item.title}

+

{item.text}

+
+ ); + })} +
+
+
+ ); +} diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx new file mode 100644 index 0000000..048be3e --- /dev/null +++ b/src/app/transactions/page.tsx @@ -0,0 +1,5 @@ +import { TransactionHistory } from "@/components/transaction-history"; + +export default function TransactionsPage() { + return ; +} diff --git a/src/components/admin-dashboard.tsx b/src/components/admin-dashboard.tsx new file mode 100644 index 0000000..a74ca62 --- /dev/null +++ b/src/components/admin-dashboard.tsx @@ -0,0 +1,126 @@ +"use client"; + +import * as React from "react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { formatZmw, shortAddress } from "@/lib/utils"; + +export function AdminDashboard() { + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [authenticated, setAuthenticated] = React.useState(false); + const [analytics, setAnalytics] = React.useState | null>(null); + const [transactions, setTransactions] = React.useState>>([]); + const [error, setError] = React.useState(null); + + async function login(event: React.FormEvent) { + event.preventDefault(); + setError(null); + const response = await fetch("/api/admin/login", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + const json = await response.json(); + if (!json.ok) { + setError(json.error?.message ?? "Login failed."); + return; + } + setAuthenticated(true); + await loadDashboard(); + } + + async function loadDashboard() { + const [analyticsResponse, txResponse] = await Promise.all([fetch("/api/admin/analytics"), fetch("/api/admin/transactions")]); + const analyticsJson = await analyticsResponse.json(); + const txJson = await txResponse.json(); + if (analyticsJson.ok) setAnalytics(analyticsJson.data); + if (txJson.ok) setTransactions(txJson.data.transactions); + } + + async function freeze(publicId: string) { + await fetch(`/api/admin/transactions/${publicId}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ action: "freeze", reason: "Flagged from admin dashboard" }) + }); + await loadDashboard(); + } + + if (!authenticated) { + return ( +
+ + + Admin login + Protect this route behind Vercel authentication or VPN for production. + + +
+
+ + setEmail(event.target.value)} /> +
+
+ + setPassword(event.target.value)} /> +
+ {error ?

{error}

: null} + +
+
+
+
+ ); + } + + return ( +
+
+ {[ + ["Transactions", analytics?.totalTransactions ?? 0], + ["Completed", analytics?.completed ?? 0], + ["Review", analytics?.manualReview ?? 0], + ["3% fees", formatZmw(analytics?.totalFeesZmw ?? 0)] + ].map(([label, value]) => ( + + + {label} + {value} + + + ))} +
+ + + + Transactions + Review failed or suspicious transactions, freeze risk, and monitor fee income. + + + {transactions.map((tx) => ( +
+
+
+

{tx.publicId}

+ {tx.status} +
+

+ {tx.type} • {tx.asset} • {formatZmw(String(tx.fiatAmountZmw))} • {shortAddress(String(tx.walletAddress))} +

+ {Array.isArray(tx.riskFlags) && tx.riskFlags.length ? ( +

{tx.riskFlags.join(", ")}

+ ) : null} +
+ +
+ ))} +
+
+
+ ); +} diff --git a/src/components/mobile-nav.tsx b/src/components/mobile-nav.tsx new file mode 100644 index 0000000..3dee07b --- /dev/null +++ b/src/components/mobile-nav.tsx @@ -0,0 +1,28 @@ +import Link from "next/link"; +import { HelpCircle, Home, ReceiptText, ShoppingCart, Wallet } from "lucide-react"; + +const items = [ + { href: "/", label: "Home", icon: Home }, + { href: "/buy", label: "Buy", icon: ShoppingCart }, + { href: "/sell", label: "Sell", icon: Wallet }, + { href: "/transactions", label: "History", icon: ReceiptText }, + { href: "/faq", label: "FAQ", icon: HelpCircle } +]; + +export function MobileNav() { + return ( + + ); +} diff --git a/src/components/providers.tsx b/src/components/providers.tsx new file mode 100644 index 0000000..7bc70bf --- /dev/null +++ b/src/components/providers.tsx @@ -0,0 +1,39 @@ +"use client"; + +import * as React from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { WagmiProvider, createConfig, http } from "wagmi"; +import { base, bsc, polygon } from "wagmi/chains"; +import { injected } from "wagmi/connectors"; + +const wagmiConfig = createConfig({ + chains: [polygon, base, bsc], + connectors: [ + injected({ + target: "metaMask" + }) + ], + transports: { + [polygon.id]: http(), + [base.id]: http(), + [bsc.id]: http() + }, + ssr: true +}); + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = React.useState(() => new QueryClient()); + + React.useEffect(() => { + const savedTheme = window.localStorage.getItem("theme"); + if (savedTheme === "dark" || (!savedTheme && window.matchMedia("(prefers-color-scheme: dark)").matches)) { + document.documentElement.classList.add("dark"); + } + }, []); + + return ( + + {children} + + ); +} diff --git a/src/components/quote-summary.tsx b/src/components/quote-summary.tsx new file mode 100644 index 0000000..220e38a --- /dev/null +++ b/src/components/quote-summary.tsx @@ -0,0 +1,42 @@ +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatZmw } from "@/lib/utils"; + +export interface QuoteSummaryData { + id: string; + fiatAmountZmw: number; + cryptoAmount: number; + feeZmw: number; + rateZmw: number; + asset: string; + expiresAt: string; +} + +export function QuoteSummary({ quote }: { quote: QuoteSummaryData }) { + return ( + + +
+ Your quote + 3% fee included +
+
+ + + + + +

Locked until {new Date(quote.expiresAt).toLocaleTimeString()}.

+
+
+ ); +} + +function Row({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/src/components/ramp-form.tsx b/src/components/ramp-form.tsx new file mode 100644 index 0000000..5ef106e --- /dev/null +++ b/src/components/ramp-form.tsx @@ -0,0 +1,195 @@ +"use client"; + +import * as React from "react"; +import { useAccount } from "wagmi"; +import { QrCode } from "lucide-react"; +import { QRCodeCanvas } from "qrcode.react"; + +import { Alert } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { NativeSelect } from "@/components/ui/select"; +import { QuoteSummary, type QuoteSummaryData } from "@/components/quote-summary"; +import { WalletConnectButton } from "@/components/wallet-connect-button"; +import { ASSETS, PAYMENT_RAILS } from "@/lib/constants"; + +type Flow = "BUY" | "SELL"; + +export function RampForm({ flow }: { flow: Flow }) { + const { address, isConnected } = useAccount(); + const [asset, setAsset] = React.useState("USDC"); + const [paymentRail, setPaymentRail] = React.useState("MTN_MOMO"); + const [fiatAmountZmw, setFiatAmountZmw] = React.useState("500"); + const [cryptoAmount, setCryptoAmount] = React.useState("10"); + const [mobileNumber, setMobileNumber] = React.useState(""); + const [quote, setQuote] = React.useState(null); + const [result, setResult] = React.useState | null>(null); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const selectedAsset = ASSETS.find((item) => item.value === asset) ?? ASSETS[1]; + + async function requestQuote() { + setLoading(true); + setError(null); + setResult(null); + try { + const response = await fetch("/api/quote", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + type: flow, + asset, + chain: selectedAsset.defaultChain, + paymentRail, + fiatAmountZmw: flow === "BUY" ? Number(fiatAmountZmw) : undefined, + cryptoAmount: flow === "SELL" ? Number(cryptoAmount) : undefined + }) + }); + const json = await response.json(); + if (!json.ok) throw new Error(json.error?.message ?? "Could not create quote."); + setQuote(json.data); + } catch (err) { + setError(err instanceof Error ? err.message : "Could not create quote."); + } finally { + setLoading(false); + } + } + + async function submitTransaction() { + if (!quote || !address) return; + setLoading(true); + setError(null); + try { + const response = await fetch(flow === "BUY" ? "/api/buy" : "/api/sell", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + quoteId: quote.id, + walletAddress: address, + mobileNumber, + idempotencyKey: `${flow}-${quote.id}-${address}` + }) + }); + const json = await response.json(); + if (!json.ok) throw new Error(json.error?.message ?? "Could not start transaction."); + setResult(json.data); + } catch (err) { + setError(err instanceof Error ? err.message : "Could not start transaction."); + } finally { + setLoading(false); + } + } + + return ( +
+ + + {flow === "BUY" ? "Buy Crypto" : "Sell Crypto"} + + {flow === "BUY" + ? "Pay from MTN or Airtel Mobile Money and receive crypto directly in MetaMask." + : "Send crypto from MetaMask and receive ZMW to MTN or Airtel Mobile Money."} + + + +
+ + +
+ +
+
+ + setAsset(event.target.value)} + options={ASSETS.filter((item) => item.value !== "BTC").map((item) => ({ + value: item.value, + label: `${item.label} - ${item.beginnerLabel}` + }))} + /> +
+
+ + setPaymentRail(event.target.value)} + options={PAYMENT_RAILS.map((rail) => ({ value: rail.value, label: rail.label }))} + /> +
+
+ +
+
+ + (flow === "BUY" ? setFiatAmountZmw(event.target.value) : setCryptoAmount(event.target.value))} + /> +
+
+ + setMobileNumber(event.target.value)} /> +
+
+ + {error ? {error} : null} + +
+ + +
+
+
+ +
+ {quote ? : } + {result ? : null} +
+
+ ); +} + +function EducationCard({ flow }: { flow: Flow }) { + return ( + + + + Beginner-safe flow + + {flow === "BUY" + ? "AfriRamp verifies the mobile money webhook before broadcasting crypto from the hot wallet." + : "AfriRamp waits for blockchain confirmations before sending a mobile money payout."} + + + + ); +} + +function ResultCard({ flow, result }: { flow: Flow; result: Record }) { + const depositAddress = result.depositAddress as string | undefined; + return ( + + + {flow === "BUY" ? "Payment started" : "Deposit address generated"} + Track this transaction from the history page using your wallet address. + + +
{JSON.stringify(result, null, 2)}
+ {depositAddress ? ( +
+ +
+ ) : null} +
+
+ ); +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx new file mode 100644 index 0000000..a7919bf --- /dev/null +++ b/src/components/site-header.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; +import { ShieldCheck } from "lucide-react"; + +import { ThemeToggle } from "@/components/theme-toggle"; +import { Button } from "@/components/ui/button"; + +export function SiteHeader() { + return ( +
+
+ + + A + + AfriRamp + + +
+
+ + Non-custodial +
+ + +
+
+
+ ); +} diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx new file mode 100644 index 0000000..d4ea4a2 --- /dev/null +++ b/src/components/theme-toggle.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; + +export function ThemeToggle() { + const [dark, setDark] = React.useState(false); + + React.useEffect(() => { + setDark(document.documentElement.classList.contains("dark")); + }, []); + + function toggleTheme() { + const next = !dark; + setDark(next); + document.documentElement.classList.toggle("dark", next); + window.localStorage.setItem("theme", next ? "dark" : "light"); + } + + return ( + + ); +} diff --git a/src/components/transaction-history.tsx b/src/components/transaction-history.tsx new file mode 100644 index 0000000..a88e5eb --- /dev/null +++ b/src/components/transaction-history.tsx @@ -0,0 +1,67 @@ +"use client"; + +import * as React from "react"; +import { useAccount } from "wagmi"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { formatZmw, shortAddress } from "@/lib/utils"; + +export function TransactionHistory() { + const { address } = useAccount(); + const [walletAddress, setWalletAddress] = React.useState(""); + const [transactions, setTransactions] = React.useState>>([]); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + if (address) setWalletAddress(address); + }, [address]); + + async function loadTransactions() { + setLoading(true); + const params = new URLSearchParams(); + if (walletAddress) params.set("walletAddress", walletAddress); + const response = await fetch(`/api/transactions?${params.toString()}`); + const json = await response.json(); + setTransactions(json.ok ? json.data.transactions : []); + setLoading(false); + } + + return ( +
+ + + Transaction history + Paste or connect your wallet to track buy and sell receipts. + + +
+ setWalletAddress(event.target.value)} /> + +
+
+ {transactions.map((tx) => ( +
+
+
+

{tx.publicId}

+

{tx.type} • {tx.asset} • {shortAddress(tx.walletAddress)}

+
+ {tx.status} +
+
+ {formatZmw(tx.fiatAmountZmw)} + {tx.cryptoAmount} {tx.asset} + {tx.blockchainTxHash ?? tx.providerReference ?? "Pending reference"} +
+
+ ))} + {!transactions.length ?

No transactions found yet.

: null} +
+
+
+
+ ); +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..d5e188d --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export function Alert({ className, ...props }: React.HTMLAttributes) { + return
; +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..6a38d64 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export function Badge({ + className, + variant = "default", + ...props +}: React.HTMLAttributes & { variant?: "default" | "muted" | "warning" | "danger" }) { + const styles = { + default: "bg-primary/12 text-primary", + muted: "bg-muted text-muted-foreground", + warning: "bg-secondary/35 text-secondary-foreground", + danger: "bg-destructive/12 text-destructive" + }; + + return ( + + ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..26d3f6c --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-2xl text-sm font-semibold transition disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow-sm hover:opacity-90", + secondary: "bg-secondary text-secondary-foreground hover:opacity-90", + outline: "border bg-card hover:bg-muted", + ghost: "hover:bg-muted", + destructive: "bg-destructive text-white hover:opacity-90" + }, + size: { + default: "h-11 px-5", + sm: "h-9 px-4", + lg: "h-13 px-7 text-base", + icon: "h-11 w-11" + } + }, + defaultVariants: { + variant: "default", + size: "default" + } + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; + } +); +Button.displayName = "Button"; + +export { buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..0603aec --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export function Card({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export function CardHeader({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export function CardTitle({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardDescription({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardContent({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardFooter({ className, ...props }: React.HTMLAttributes) { + return
; +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..1b36d2e --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes {} + +export const Input = React.forwardRef(({ className, type, ...props }, ref) => ( + +)); +Input.displayName = "Input"; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..5aad229 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,12 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; + +import { cn } from "@/lib/utils"; + +export const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..7845826 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface NativeSelectProps extends React.SelectHTMLAttributes { + options: Array<{ label: string; value: string }>; +} + +export function NativeSelect({ className, options, ...props }: NativeSelectProps) { + return ( + + ); +} diff --git a/src/components/wallet-connect-button.tsx b/src/components/wallet-connect-button.tsx new file mode 100644 index 0000000..60266f0 --- /dev/null +++ b/src/components/wallet-connect-button.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useAccount, useConnect, useDisconnect } from "wagmi"; +import { Wallet } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { shortAddress } from "@/lib/utils"; + +export function WalletConnectButton() { + const { address, isConnected } = useAccount(); + const { connect, connectors, isPending } = useConnect(); + const { disconnect } = useDisconnect(); + const connector = connectors[0]; + + if (isConnected) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/lib/admin-auth.ts b/src/lib/admin-auth.ts new file mode 100644 index 0000000..c4e7e03 --- /dev/null +++ b/src/lib/admin-auth.ts @@ -0,0 +1,47 @@ +import { cookies } from "next/headers"; +import bcrypt from "bcryptjs"; +import { jwtVerify, SignJWT } from "jose"; + +import { getServerEnv } from "@/lib/env"; + +const ADMIN_COOKIE = "afriramp_admin"; + +function getSecret() { + const secret = getServerEnv().ADMIN_JWT_SECRET; + if (!secret) throw new Error("ADMIN_JWT_SECRET is not configured."); + return new TextEncoder().encode(secret); +} + +export async function verifyAdminPassword(email: string, password: string) { + const env = getServerEnv(); + if (!env.ADMIN_EMAIL || !env.ADMIN_PASSWORD_HASH) return false; + if (email.toLowerCase() !== env.ADMIN_EMAIL.toLowerCase()) return false; + return bcrypt.compare(password, env.ADMIN_PASSWORD_HASH); +} + +export async function createAdminToken(email: string) { + return new SignJWT({ email, role: "admin" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("8h") + .sign(getSecret()); +} + +export async function setAdminCookie(token: string) { + const cookieStore = await cookies(); + cookieStore.set(ADMIN_COOKIE, token, { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + maxAge: 8 * 60 * 60, + path: "/" + }); +} + +export async function requireAdmin() { + const cookieStore = await cookies(); + const token = cookieStore.get(ADMIN_COOKIE)?.value; + if (!token) throw new Error("Admin authentication required."); + const verified = await jwtVerify(token, getSecret()); + return verified.payload as { email: string; role: "admin" }; +} diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..8d960c3 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import { ZodError } from "zod"; + +export function ok(data: T, init?: ResponseInit) { + return NextResponse.json({ ok: true, data }, init); +} + +export function fail(message: string, status = 400, code = "BAD_REQUEST") { + return NextResponse.json({ ok: false, error: { code, message } }, { status }); +} + +export function handleApiError(error: unknown) { + if (error instanceof ZodError) { + return fail(error.issues[0]?.message ?? "Invalid request.", 422, "VALIDATION_ERROR"); + } + + console.error(error); + return fail("Something went wrong. Please try again or contact support.", 500, "INTERNAL_ERROR"); +} + +export async function readJson(request: Request): Promise { + try { + return (await request.json()) as T; + } catch { + throw new Error("Invalid JSON body."); + } +} diff --git a/src/lib/audit.ts b/src/lib/audit.ts new file mode 100644 index 0000000..56c9c9a --- /dev/null +++ b/src/lib/audit.ts @@ -0,0 +1,33 @@ +import type { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/prisma"; + +export async function auditLog(input: { + actorType: "system" | "user" | "admin" | "webhook"; + actorId?: string; + action: string; + entityType: string; + entityId: string; + before?: Prisma.InputJsonValue; + after?: Prisma.InputJsonValue; + ipAddress?: string | null; + userAgent?: string | null; + userId?: string; + transactionId?: string; +}) { + await prisma.auditLog.create({ + data: { + actorType: input.actorType, + actorId: input.actorId, + action: input.action, + entityType: input.entityType, + entityId: input.entityId, + before: input.before, + after: input.after, + ipAddress: input.ipAddress ?? undefined, + userAgent: input.userAgent ?? undefined, + userId: input.userId, + transactionId: input.transactionId + } + }); +} diff --git a/src/lib/blockchain/chains.ts b/src/lib/blockchain/chains.ts new file mode 100644 index 0000000..487804f --- /dev/null +++ b/src/lib/blockchain/chains.ts @@ -0,0 +1,17 @@ +import type { Chain as PrismaChain } from "@prisma/client"; +import { base, bsc, polygon } from "viem/chains"; + +import { getServerEnv } from "@/lib/env"; + +export function getViemChain(chain: PrismaChain) { + if (chain === "BASE") return base; + if (chain === "BNB") return bsc; + return polygon; +} + +export function getRpcUrl(chain: PrismaChain) { + const env = getServerEnv(); + if (chain === "BASE") return env.BASE_RPC_URL; + if (chain === "BNB") return env.BNB_RPC_URL; + return env.POLYGON_RPC_URL; +} diff --git a/src/lib/blockchain/hot-wallet.ts b/src/lib/blockchain/hot-wallet.ts new file mode 100644 index 0000000..2bf9b5d --- /dev/null +++ b/src/lib/blockchain/hot-wallet.ts @@ -0,0 +1,74 @@ +import type { Asset, Chain } from "@prisma/client"; +import { createPublicClient, createWalletClient, erc20Abi, http, parseUnits } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; + +import { TOKEN_CONTRACTS, TOKEN_DECIMALS } from "@/lib/constants"; +import { getServerEnv } from "@/lib/env"; +import { getRpcUrl, getViemChain } from "@/lib/blockchain/chains"; + +export function allocateDepositAddress() { + const env = getServerEnv(); + const addresses = (env.DEPOSIT_WALLET_ADDRESSES || env.HOT_WALLET_ADDRESS || "") + .split(",") + .map((address) => address.trim()) + .filter(Boolean); + if (!addresses.length) return null; + return addresses[Math.floor(Math.random() * addresses.length)]; +} + +export async function sendTokenFromHotWallet(input: { + asset: Asset; + chain: Chain; + to: `0x${string}`; + amount: string; + reference: string; +}) { + const env = getServerEnv(); + + if (input.asset === "BTC" || input.chain === "BITCOIN") { + throw new Error("Native Bitcoin disbursement is intentionally disabled in the MVP. Use WBTC on Polygon first."); + } + + const contract = TOKEN_CONTRACTS[input.asset as Exclude]?.[input.chain]; + if (!contract) throw new Error(`${input.asset} is not configured on ${input.chain}.`); + + if (env.ENABLE_BLOCKCHAIN_BROADCAST !== "true") { + return { + hash: `simulated-${input.reference}`, + simulated: true + }; + } + + if (!env.HOT_WALLET_PRIVATE_KEY) throw new Error("HOT_WALLET_PRIVATE_KEY is not configured."); + const account = privateKeyToAccount(env.HOT_WALLET_PRIVATE_KEY as `0x${string}`); + const chain = getViemChain(input.chain); + const transport = http(getRpcUrl(input.chain)); + const walletClient = createWalletClient({ account, chain, transport }); + const amount = parseUnits(input.amount, TOKEN_DECIMALS[input.asset]); + + const hash = await walletClient.writeContract({ + address: contract, + abi: erc20Abi, + functionName: "transfer", + args: [input.to, amount] + }); + + return { hash, simulated: false }; +} + +export async function getHotWalletBalance(input: { asset: Exclude; chain: Chain }) { + const env = getServerEnv(); + const address = env.HOT_WALLET_ADDRESS as `0x${string}` | undefined; + const contract = TOKEN_CONTRACTS[input.asset]?.[input.chain]; + if (!address || !contract) return null; + + const client = createPublicClient({ chain: getViemChain(input.chain), transport: http(getRpcUrl(input.chain)) }); + const balance = await client.readContract({ + address: contract, + abi: erc20Abi, + functionName: "balanceOf", + args: [address] + }); + + return balance.toString(); +} diff --git a/src/lib/blockchain/verify.ts b/src/lib/blockchain/verify.ts new file mode 100644 index 0000000..c563f5d --- /dev/null +++ b/src/lib/blockchain/verify.ts @@ -0,0 +1,39 @@ +import type { Asset, Chain } from "@prisma/client"; +import { createPublicClient, erc20Abi, getAddress, http, parseUnits } from "viem"; + +import { TOKEN_CONTRACTS, TOKEN_DECIMALS } from "@/lib/constants"; +import { getRpcUrl, getViemChain } from "@/lib/blockchain/chains"; + +export async function verifyTokenTransfer(input: { + chain: Chain; + asset: Exclude; + txHash: `0x${string}`; + expectedFrom: `0x${string}`; + expectedTo: `0x${string}`; + expectedAmount: string; +}) { + const contract = TOKEN_CONTRACTS[input.asset]?.[input.chain]; + if (!contract) throw new Error(`${input.asset} is not configured on ${input.chain}.`); + + const client = createPublicClient({ chain: getViemChain(input.chain), transport: http(getRpcUrl(input.chain)) }); + const receipt = await client.getTransactionReceipt({ hash: input.txHash }); + if (receipt.status !== "success") return { valid: false, confirmations: 0 }; + + const transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + const expectedAmount = parseUnits(input.expectedAmount, TOKEN_DECIMALS[input.asset]); + const valid = receipt.logs.some((log) => { + if (log.address.toLowerCase() !== contract.toLowerCase() || log.topics[0] !== transferTopic) return false; + const from = `0x${log.topics[1]?.slice(26)}` as `0x${string}`; + const to = `0x${log.topics[2]?.slice(26)}` as `0x${string}`; + const amount = BigInt(log.data); + return ( + getAddress(from) === getAddress(input.expectedFrom) && + getAddress(to) === getAddress(input.expectedTo) && + amount >= expectedAmount + ); + }); + + const blockNumber = await client.getBlockNumber(); + const confirmations = receipt.blockNumber ? Number(blockNumber - receipt.blockNumber + 1n) : 0; + return { valid, confirmations }; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..3fe10ee --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,43 @@ +import type { Chain as PrismaChain, Asset, PaymentRail } from "@prisma/client"; + +export const PLATFORM_FEE_RATE = 0.03; + +export const PAYMENT_RAILS: Array<{ value: PaymentRail; label: string; hint: string }> = [ + { value: "MTN_MOMO", label: "MTN Mobile Money", hint: "Fast collections and payouts" }, + { value: "AIRTEL_MONEY", label: "Airtel Money", hint: "Zambia Airtel Money support" } +]; + +export const ASSETS: Array<{ value: Asset; label: string; beginnerLabel: string; defaultChain: PrismaChain }> = [ + { value: "USDT", label: "USDT", beginnerLabel: "Tether USD", defaultChain: "POLYGON" }, + { value: "USDC", label: "USDC", beginnerLabel: "USD Coin", defaultChain: "BASE" }, + { value: "WBTC", label: "Wrapped BTC", beginnerLabel: "Bitcoin exposure on Polygon", defaultChain: "POLYGON" }, + { value: "BTC", label: "Bitcoin", beginnerLabel: "Bitcoin mainnet - later phase", defaultChain: "BITCOIN" } +]; + +export const SUPPORTED_EVM_CHAINS: PrismaChain[] = ["POLYGON", "BASE", "BNB"]; + +export const TOKEN_CONTRACTS: Record, Partial>> = { + USDT: { + POLYGON: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + BNB: "0x55d398326f99059fF775485246999027B3197955" + }, + USDC: { + BASE: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + POLYGON: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" + }, + WBTC: { + POLYGON: "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6" + } +}; + +export const TOKEN_DECIMALS: Record = { + USDT: 6, + USDC: 6, + WBTC: 8, + BTC: 8 +}; + +export const TRANSACTION_STEPS = { + BUY: ["Quote", "Mobile money paid", "Crypto sent", "Complete"], + SELL: ["Quote", "Crypto received", "Mobile money sent", "Complete"] +} as const; diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..dbc687e --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +const serverEnvSchema = z.object({ + DATABASE_URL: z.string().min(1), + REDIS_URL: z.string().optional(), + ADMIN_EMAIL: z.string().email().optional(), + ADMIN_PASSWORD_HASH: z.string().optional(), + ADMIN_JWT_SECRET: z.string().min(16).optional(), + MOBILE_MONEY_PROVIDER: z.enum(["mock", "flutterwave", "paychangu", "lipila", "pesapal"]).default("mock"), + MOBILE_MONEY_WEBHOOK_SECRET: z.string().min(8).optional(), + MOBILE_MONEY_API_BASE_URL: z.string().url().optional().or(z.literal("")), + MOBILE_MONEY_API_KEY: z.string().optional(), + MOBILE_MONEY_API_SECRET: z.string().optional(), + MOBILE_MONEY_CALLBACK_URL: z.string().url().optional().or(z.literal("")), + ENABLE_BLOCKCHAIN_BROADCAST: z.enum(["true", "false"]).default("false"), + HOT_WALLET_PRIVATE_KEY: z.string().optional(), + HOT_WALLET_ADDRESS: z.string().optional(), + DEPOSIT_WALLET_ADDRESSES: z.string().optional(), + POLYGON_RPC_URL: z.string().url().default("https://polygon-rpc.com"), + BASE_RPC_URL: z.string().url().default("https://mainnet.base.org"), + BNB_RPC_URL: z.string().url().default("https://bsc-dataseed.binance.org"), + MAX_SINGLE_TRANSACTION_ZMW: z.coerce.number().positive().default(25000), + QUOTE_LOCK_SECONDS: z.coerce.number().positive().default(300), + REQUIRED_EVM_CONFIRMATIONS: z.coerce.number().int().positive().default(12) +}); + +export type ServerEnv = z.infer; + +export function getServerEnv(): ServerEnv { + return serverEnvSchema.parse(process.env); +} diff --git a/src/lib/idempotency.ts b/src/lib/idempotency.ts new file mode 100644 index 0000000..18555ef --- /dev/null +++ b/src/lib/idempotency.ts @@ -0,0 +1,25 @@ +import { prisma } from "@/lib/prisma"; + +export async function reserveIdempotencyKey(input: { key: string; route: string }) { + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); + try { + await prisma.idempotencyKey.create({ + data: { + key: input.key, + route: input.route, + expiresAt + } + }); + return { reused: false }; + } catch { + const existing = await prisma.idempotencyKey.findUnique({ where: { key: input.key } }); + return { reused: true, response: existing?.response }; + } +} + +export async function storeIdempotentResponse(key: string, response: unknown) { + await prisma.idempotencyKey.update({ + where: { key }, + data: { response: response as never } + }); +} diff --git a/src/lib/payments/http-provider.ts b/src/lib/payments/http-provider.ts new file mode 100644 index 0000000..e38b069 --- /dev/null +++ b/src/lib/payments/http-provider.ts @@ -0,0 +1,67 @@ +import { getServerEnv } from "@/lib/env"; +import { verifyHmacSignature } from "@/lib/security"; +import type { MobileMoneyProvider, MobileMoneyProviderName, NormalizedWebhook } from "@/lib/payments/types"; + +export class HttpMobileMoneyProvider implements MobileMoneyProvider { + constructor(public readonly name: Exclude) {} + + async initiateCollection(input: Parameters[0]) { + return this.request("/collections", input, `collect-${input.reference}`); + } + + async initiatePayout(input: Parameters[0]) { + return this.request("/payouts", input, `payout-${input.reference}`); + } + + async parseWebhook(rawBody: string, headers: Headers): Promise { + const env = getServerEnv(); + if (env.MOBILE_MONEY_WEBHOOK_SECRET) { + const signature = + headers.get("x-signature") ?? headers.get("x-webhook-signature") ?? headers.get("verif-hash") ?? headers.get("x-afriramp-signature"); + if (!verifyHmacSignature(env.MOBILE_MONEY_WEBHOOK_SECRET, rawBody, signature)) { + throw new Error("Invalid provider webhook signature."); + } + } + + const payload = JSON.parse(rawBody) as Record; + const reference = String(payload.reference ?? payload.tx_ref ?? payload.transaction_reference ?? ""); + const eventId = String(payload.id ?? payload.event_id ?? payload.transaction_id ?? reference); + const providerStatus = String(payload.status ?? payload.event ?? "").toLowerCase(); + + return { + eventId, + reference, + status: providerStatus.includes("success") || providerStatus.includes("complete") ? "successful" : providerStatus.includes("fail") ? "failed" : "pending", + raw: payload + }; + } + + private async request(path: string, input: object, fallbackReference: string) { + const env = getServerEnv(); + if (!env.MOBILE_MONEY_API_BASE_URL || !env.MOBILE_MONEY_API_KEY) { + throw new Error(`${this.name} mobile money credentials are not configured.`); + } + + const response = await fetch(`${env.MOBILE_MONEY_API_BASE_URL.replace(/\/$/, "")}${path}`, { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${env.MOBILE_MONEY_API_KEY}`, + "x-api-secret": env.MOBILE_MONEY_API_SECRET ?? "" + }, + body: JSON.stringify(input) + }); + const json = (await response.json().catch(() => ({}))) as Record; + if (!response.ok) { + throw new Error(`${this.name} request failed with status ${response.status}.`); + } + + const status = String(json.status ?? "").toLowerCase(); + return { + provider: this.name, + reference: String(json.reference ?? json.tx_ref ?? json.id ?? fallbackReference), + status: status.includes("success") ? ("successful" as const) : status.includes("fail") ? ("failed" as const) : ("pending" as const), + raw: json + }; + } +} diff --git a/src/lib/payments/index.ts b/src/lib/payments/index.ts new file mode 100644 index 0000000..e30fe6a --- /dev/null +++ b/src/lib/payments/index.ts @@ -0,0 +1,10 @@ +import { getServerEnv } from "@/lib/env"; +import { HttpMobileMoneyProvider } from "@/lib/payments/http-provider"; +import { MockMobileMoneyProvider } from "@/lib/payments/mock"; +import type { MobileMoneyProvider } from "@/lib/payments/types"; + +export function getMobileMoneyProvider(): MobileMoneyProvider { + const provider = getServerEnv().MOBILE_MONEY_PROVIDER; + if (provider === "mock") return new MockMobileMoneyProvider(); + return new HttpMobileMoneyProvider(provider); +} diff --git a/src/lib/payments/mock.ts b/src/lib/payments/mock.ts new file mode 100644 index 0000000..643e41f --- /dev/null +++ b/src/lib/payments/mock.ts @@ -0,0 +1,42 @@ +import { verifyHmacSignature } from "@/lib/security"; +import type { MobileMoneyProvider, NormalizedWebhook } from "@/lib/payments/types"; + +export class MockMobileMoneyProvider implements MobileMoneyProvider { + name = "mock" as const; + + async initiateCollection(input: Parameters[0]) { + return { + provider: this.name, + reference: `mock-collect-${input.reference}`, + status: "pending" as const, + raw: { + message: "Mock collection created. POST a webhook JSON body signed with HMAC-SHA256 in x-afriramp-signature.", + examplePayload: { eventId: `evt-${input.reference}`, reference: `mock-collect-${input.reference}`, status: "successful" } + } + }; + } + + async initiatePayout(input: Parameters[0]) { + return { + provider: this.name, + reference: `mock-payout-${input.reference}`, + status: "pending" as const, + raw: { message: "Mock payout queued." } + }; + } + + async parseWebhook(rawBody: string, headers: Headers): Promise { + const secret = process.env.MOBILE_MONEY_WEBHOOK_SECRET ?? "local-webhook-secret"; + if (!verifyHmacSignature(secret, rawBody, headers.get("x-afriramp-signature"))) { + throw new Error("Invalid webhook signature."); + } + + const payload = JSON.parse(rawBody) as { eventId?: string; reference?: string; status?: string }; + return { + eventId: payload.eventId ?? crypto.randomUUID(), + reference: payload.reference ?? "", + status: payload.status === "failed" ? "failed" : payload.status === "pending" ? "pending" : "successful", + raw: payload + }; + } +} diff --git a/src/lib/payments/types.ts b/src/lib/payments/types.ts new file mode 100644 index 0000000..38b1aef --- /dev/null +++ b/src/lib/payments/types.ts @@ -0,0 +1,35 @@ +import type { PaymentRail } from "@prisma/client"; + +export type MobileMoneyProviderName = "mock" | "flutterwave" | "paychangu" | "lipila" | "pesapal"; + +export interface CollectionRequest { + amountZmw: number; + mobileNumber: string; + rail: PaymentRail; + reference: string; + callbackUrl?: string; + description: string; +} + +export interface PayoutRequest extends CollectionRequest {} + +export interface ProviderResult { + provider: MobileMoneyProviderName; + reference: string; + status: "pending" | "successful" | "failed"; + raw: unknown; +} + +export interface NormalizedWebhook { + eventId: string; + reference: string; + status: "successful" | "failed" | "pending"; + raw: unknown; +} + +export interface MobileMoneyProvider { + name: MobileMoneyProviderName; + initiateCollection(input: CollectionRequest): Promise; + initiatePayout(input: PayoutRequest): Promise; + parseWebhook(rawBody: string, headers: Headers): Promise; +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..82aaed4 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,19 @@ +import { PrismaClient } from "@prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; + +const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; + +function createPrismaClient() { + const connectionString = process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5432/afriramp?schema=public"; + const adapter = new PrismaPg({ connectionString }); + return new PrismaClient({ + adapter, + log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"] + }); +} + +export const prisma = globalForPrisma.prisma ?? createPrismaClient(); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/src/lib/queue.ts b/src/lib/queue.ts new file mode 100644 index 0000000..bcac24e --- /dev/null +++ b/src/lib/queue.ts @@ -0,0 +1,19 @@ +import Redis from "ioredis"; + +let redis: Redis | null = null; + +function getRedis() { + if (!process.env.REDIS_URL) return null; + redis ??= new Redis(process.env.REDIS_URL, { maxRetriesPerRequest: 2, lazyConnect: true }); + return redis; +} + +export async function enqueueJob(queue: "crypto-disbursement" | "mobile-money-payout" | "notifications", payload: unknown) { + const client = getRedis(); + const job = JSON.stringify({ payload, createdAt: new Date().toISOString() }); + if (!client) { + console.info(`[queue:${queue}] Redis not configured; job logged for worker pickup`, job); + return; + } + await client.lpush(`queue:${queue}`, job); +} diff --git a/src/lib/rates.ts b/src/lib/rates.ts new file mode 100644 index 0000000..ba9f8bd --- /dev/null +++ b/src/lib/rates.ts @@ -0,0 +1,93 @@ +import type { Asset, TransactionType } from "@prisma/client"; + +import { PLATFORM_FEE_RATE } from "@/lib/constants"; + +const ASSET_PRICE_IDS: Record = { + USDT: "tether", + USDC: "usd-coin", + WBTC: "wrapped-bitcoin", + BTC: "bitcoin" +}; + +const FALLBACK_USD_PRICES: Record = { + USDT: 1, + USDC: 1, + WBTC: 103000, + BTC: 103000 +}; + +let cache: { expiresAt: number; usdToZmw: number; prices: Record } | null = null; + +export async function getExchangeRates() { + if (cache && cache.expiresAt > Date.now()) return cache; + + const [prices, usdToZmw] = await Promise.all([fetchCryptoUsdPrices(), fetchUsdToZmwRate()]); + cache = { + prices, + usdToZmw, + expiresAt: Date.now() + 60_000 + }; + return cache; +} + +async function fetchCryptoUsdPrices(): Promise> { + try { + const ids = Object.values(ASSET_PRICE_IDS).join(","); + const response = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`, { + next: { revalidate: 60 } + }); + if (!response.ok) throw new Error("CoinGecko rate request failed"); + const json = (await response.json()) as Record; + return { + USDT: json[ASSET_PRICE_IDS.USDT]?.usd ?? FALLBACK_USD_PRICES.USDT, + USDC: json[ASSET_PRICE_IDS.USDC]?.usd ?? FALLBACK_USD_PRICES.USDC, + WBTC: json[ASSET_PRICE_IDS.WBTC]?.usd ?? FALLBACK_USD_PRICES.WBTC, + BTC: json[ASSET_PRICE_IDS.BTC]?.usd ?? FALLBACK_USD_PRICES.BTC + }; + } catch { + return FALLBACK_USD_PRICES; + } +} + +async function fetchUsdToZmwRate() { + try { + const response = await fetch("https://open.er-api.com/v6/latest/USD", { next: { revalidate: 300 } }); + if (!response.ok) throw new Error("FX rate request failed"); + const json = (await response.json()) as { rates?: { ZMW?: number } }; + return json.rates?.ZMW ?? 27; + } catch { + return 27; + } +} + +export async function createQuoteCalculation(input: { + type: TransactionType; + asset: Asset; + fiatAmountZmw?: number; + cryptoAmount?: number; +}) { + const rates = await getExchangeRates(); + const rateZmw = rates.prices[input.asset] * rates.usdToZmw; + + if (input.type === "BUY") { + const fiatAmountZmw = input.fiatAmountZmw ?? 0; + const feeZmw = fiatAmountZmw * PLATFORM_FEE_RATE; + const netZmw = fiatAmountZmw - feeZmw; + return { + fiatAmountZmw, + feeZmw, + rateZmw, + cryptoAmount: netZmw / rateZmw + }; + } + + const cryptoAmount = input.cryptoAmount ?? 0; + const grossZmw = cryptoAmount * rateZmw; + const feeZmw = grossZmw * PLATFORM_FEE_RATE; + return { + fiatAmountZmw: grossZmw - feeZmw, + feeZmw, + rateZmw, + cryptoAmount + }; +} diff --git a/src/lib/risk.ts b/src/lib/risk.ts new file mode 100644 index 0000000..70f4525 --- /dev/null +++ b/src/lib/risk.ts @@ -0,0 +1,28 @@ +import type { PaymentRail } from "@prisma/client"; + +import { getServerEnv } from "@/lib/env"; + +export function assessBasicRisk(input: { + fiatAmountZmw: number; + mobileNumber: string; + walletAddress: string; + paymentRail: PaymentRail; +}) { + const env = getServerEnv(); + const flags: string[] = []; + + if (input.fiatAmountZmw > env.MAX_SINGLE_TRANSACTION_ZMW) { + flags.push("amount_above_single_transaction_limit"); + } + + const airtelPrefixes = ["+26097", "+26077"]; + const mtnPrefixes = ["+26096", "+26076", "+26095"]; + if (input.paymentRail === "AIRTEL_MONEY" && !airtelPrefixes.some((prefix) => input.mobileNumber.startsWith(prefix))) { + flags.push("phone_prefix_payment_rail_mismatch"); + } + if (input.paymentRail === "MTN_MOMO" && !mtnPrefixes.some((prefix) => input.mobileNumber.startsWith(prefix))) { + flags.push("phone_prefix_payment_rail_mismatch"); + } + + return flags; +} diff --git a/src/lib/security.ts b/src/lib/security.ts new file mode 100644 index 0000000..da7585d --- /dev/null +++ b/src/lib/security.ts @@ -0,0 +1,41 @@ +import crypto from "node:crypto"; + +import { isAddress } from "viem"; + +export function hashValue(value: string) { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +export function safeCompare(a: string, b: string) { + const left = Buffer.from(a); + const right = Buffer.from(b); + if (left.length !== right.length) return false; + return crypto.timingSafeEqual(left, right); +} + +export function hmacSha256(secret: string, payload: string) { + return crypto.createHmac("sha256", secret).update(payload).digest("hex"); +} + +export function verifyHmacSignature(secret: string, rawBody: string, signature: string | null) { + if (!secret || !signature) return false; + const expected = hmacSha256(secret, rawBody); + const normalized = signature.replace(/^sha256=/, ""); + return safeCompare(expected, normalized); +} + +export function normalizePhone(phone: string) { + const digits = phone.replace(/\D/g, ""); + if (digits.startsWith("260")) return `+${digits}`; + if (digits.startsWith("0")) return `+260${digits.slice(1)}`; + return `+260${digits}`; +} + +export function normalizeWallet(address: string) { + if (!isAddress(address)) throw new Error("Invalid EVM wallet address."); + return address.toLowerCase(); +} + +export function createPublicId(prefix: "BUY" | "SELL") { + return `${prefix}-${crypto.randomBytes(5).toString("hex").toUpperCase()}`; +} diff --git a/src/lib/state-machine.ts b/src/lib/state-machine.ts new file mode 100644 index 0000000..fccdd69 --- /dev/null +++ b/src/lib/state-machine.ts @@ -0,0 +1,32 @@ +import type { TransactionStatus, TransactionType } from "@prisma/client"; + +const transitions: Record = { + QUOTE_CREATED: ["PAYMENT_PENDING", "CRYPTO_DEPOSIT_PENDING", "EXPIRED", "FAILED", "FROZEN"], + PAYMENT_PENDING: ["PAYMENT_CONFIRMED", "FAILED", "EXPIRED", "FROZEN", "MANUAL_REVIEW"], + PAYMENT_CONFIRMED: ["CRYPTO_DISBURSEMENT_PENDING", "FAILED", "FROZEN", "MANUAL_REVIEW"], + CRYPTO_DISBURSEMENT_PENDING: ["CRYPTO_SENT", "FAILED", "FROZEN", "MANUAL_REVIEW"], + CRYPTO_SENT: ["COMPLETED", "FAILED", "MANUAL_REVIEW"], + CRYPTO_DEPOSIT_PENDING: ["CRYPTO_DEPOSIT_CONFIRMED", "FAILED", "EXPIRED", "FROZEN", "MANUAL_REVIEW"], + CRYPTO_DEPOSIT_CONFIRMED: ["PAYOUT_PENDING", "FAILED", "FROZEN", "MANUAL_REVIEW"], + PAYOUT_PENDING: ["PAYOUT_SENT", "FAILED", "FROZEN", "MANUAL_REVIEW"], + PAYOUT_SENT: ["COMPLETED", "FAILED", "MANUAL_REVIEW"], + COMPLETED: [], + FAILED: ["MANUAL_REVIEW"], + FROZEN: ["MANUAL_REVIEW", "FAILED"], + EXPIRED: ["MANUAL_REVIEW"], + MANUAL_REVIEW: ["PAYMENT_PENDING", "CRYPTO_DEPOSIT_PENDING", "PAYOUT_PENDING", "FAILED", "FROZEN", "COMPLETED"] +}; + +export function assertTransition(from: TransactionStatus, to: TransactionStatus) { + if (!transitions[from].includes(to)) { + throw new Error(`Invalid transaction transition from ${from} to ${to}.`); + } +} + +export function nextInitialStatus(type: TransactionType) { + return type === "BUY" ? "PAYMENT_PENDING" : "CRYPTO_DEPOSIT_PENDING"; +} + +export function isTerminalStatus(status: TransactionStatus) { + return ["COMPLETED", "FAILED", "EXPIRED"].includes(status); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..405f54a --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,24 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatZmw(value: number | string) { + return new Intl.NumberFormat("en-ZM", { + style: "currency", + currency: "ZMW", + maximumFractionDigits: 2 + }).format(Number(value)); +} + +export function shortAddress(address?: string | null) { + if (!address) return ""; + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +export function toNumber(value: unknown, fallback = 0) { + const number = Number(value); + return Number.isFinite(number) ? number : fallback; +} diff --git a/src/lib/validators.ts b/src/lib/validators.ts new file mode 100644 index 0000000..21be9ec --- /dev/null +++ b/src/lib/validators.ts @@ -0,0 +1,41 @@ +import { Asset, Chain, PaymentRail, TransactionType } from "@prisma/client"; +import { z } from "zod"; + +export const walletAddressSchema = z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Connect a valid MetaMask EVM wallet address."); + +export const zambiaMobileNumberSchema = z + .string() + .trim() + .regex(/^(\+?260|0)?(76|77|96|95|97)\d{7}$/, "Enter a valid Zambia MTN or Airtel mobile number."); + +export const quoteRequestSchema = z.object({ + type: z.nativeEnum(TransactionType), + fiatAmountZmw: z.coerce.number().positive().max(250000).optional(), + cryptoAmount: z.coerce.number().positive().max(1000000).optional(), + asset: z.nativeEnum(Asset), + chain: z.nativeEnum(Chain), + paymentRail: z.nativeEnum(PaymentRail) +}); + +export const buyRequestSchema = z.object({ + quoteId: z.string().min(8), + walletAddress: walletAddressSchema, + mobileNumber: zambiaMobileNumberSchema, + idempotencyKey: z.string().min(12).max(128) +}); + +export const sellRequestSchema = buyRequestSchema.extend({ + walletAddress: walletAddressSchema +}); + +export const transactionQuerySchema = z.object({ + walletAddress: walletAddressSchema.optional(), + publicId: z.string().optional() +}); + +export const adminLoginSchema = z.object({ + email: z.string().email(), + password: z.string().min(8) +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..752a139 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "es2022" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..8f0b196 --- /dev/null +++ b/vercel.json @@ -0,0 +1,5 @@ +{ + "framework": "nextjs", + "buildCommand": "npm run build", + "installCommand": "npm ci" +}