diff --git a/README.md b/README.md index f8b3add..18e69c4 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ Today this runs entirely on trust — no contracts, no guarantees, frequent frau │ Next.js App │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ Public Pages│ │ API Routes │ │ Server Services │ │ -│ │ /circles │ │ /api/circles│ │ circle.service │ │ -│ │ /dashboard │ │ /api/auth │ │ payout.service │ │ -│ │ /auth/login │ │ /api/cron │ │ scheduler │ │ +│ │ /circles │ │/api/v1/circle│ │ circle.service │ │ +│ │ /dashboard │ │/api/auth │ │ payout.service │ │ +│ │ /auth/login │ │/api/v1/cron │ │ scheduler │ │ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ │ │ @@ -73,11 +73,13 @@ src/ │ ├── dashboard/ # User's circles │ ├── auth/login/ # Phone OTP login │ └── api/ -│ ├── circles/ # Circle CRUD + join -│ ├── auth/ # OTP + NextAuth -│ └── cron/cycle/ # Payout scheduler +│ ├── auth/ # NextAuth route handlers (exempt from v1 prefix) +│ └── v1/ # Versioned API routes (/api/v1/) +│ ├── circles/ # Circle CRUD + join + waitlist +│ ├── auth/ # OTP v1 routes +│ └── cron/cycle/ # Payout scheduler ├── server/ -│ ├── services/ # circle, payout, scheduler +│ ├── services/ # circle, payout, scheduler, waitlist │ ├── middleware/ # Auth, rate limiting │ └── config/ ├── components/ @@ -95,6 +97,21 @@ scripts/ --- +## API Versioning + +All API endpoints are strictly versioned under the `/api/v1/` path to ensure backward compatibility and smooth future integrations. + +### Standard Redirection & Deprecation +- **Dynamic Redirects**: Any unversioned legacy request directed at `/api/*` is dynamically intercepted by the Next.js middleware and redirected to `/api/v1/*` automatically. + - **GET requests**: Responds with `301 Moved Permanently`. + - **Non-GET requests** (POST, PUT, DELETE, PATCH): Responds with `308 Permanent Redirect` to safely preserve the request body and HTTP method. +- **Deprecation Headers**: Legacy redirected responses automatically append: + - `X-API-Deprecated: true` + - `X-API-Deprecation-Info: This endpoint is deprecated. Use /api/v1/{endpoint} instead.` +- **Exceptions**: Authentication endpoints (`/api/auth/*` for NextAuth) are explicitly excluded from redirection to ensure login flows remain uninterrupted. + +--- + ## Smart Contract The Ajo contract (`contracts/ajo/`) handles the full circle lifecycle: diff --git a/instrumentation.ts b/instrumentation.ts index 236ebd5..bc2c0b7 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -3,14 +3,14 @@ export async function register() { await import("./sentry.server.config"); if (process.env.NODE_ENV === "production") { - const { default: runner } = await import("node-pg-migrate"); + const { runner } = await import("node-pg-migrate"); const path = await import("path"); await runner({ databaseUrl: process.env.DATABASE_URL!, dir: path.join(process.cwd(), "migrations"), direction: "up", migrationsTable: "pgmigrations", - log: (msg) => console.log("[migrate]", msg), + log: (msg: any) => console.log("[migrate]", msg), }); } diff --git a/jest.config.js b/jest.config.js index f7a5996..847cc38 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,7 +5,10 @@ const createJestConfig = nextJest({ dir: "./" }); const config = { setupFilesAfterEnv: ["/jest.setup.ts"], testEnvironment: "jest-environment-jsdom", - moduleNameMapper: { "^@/(.*)$": "/src/$1" }, + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + "^jose$": require.resolve("jose"), + }, testPathIgnorePatterns: ["/node_modules/", "/.next/", "/e2e/"], collectCoverageFrom: [ "src/**/*.{ts,tsx}", diff --git a/jest.setup.ts b/jest.setup.ts index 4897d8b..7406803 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -3,3 +3,51 @@ import { TextEncoder, TextDecoder } from "util"; global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder as any; + +const timers = require("timers"); +global.setImmediate = global.setImmediate || timers.setImmediate; +global.clearImmediate = global.clearImmediate || timers.clearImmediate; + +// Polyfill fetch globals for JSDOM environment using next's compiled primitives +const primitives = require("next/dist/compiled/@edge-runtime/primitives"); +global.Request = primitives.Request; +global.Response = primitives.Response; +global.Headers = primitives.Headers; +global.fetch = primitives.fetch; + +// Global mocks for Redis/ioredis to prevent tests from initiating real connections +jest.mock("ioredis", () => { + return jest.fn().mockImplementation(() => { + return { + on: jest.fn(), + info: jest.fn().mockResolvedValue("redis_version:7.0.0"), + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue("OK"), + del: jest.fn().mockResolvedValue(1), + quit: jest.fn().mockResolvedValue("OK"), + disconnect: jest.fn().mockResolvedValue("OK"), + zRemRangeByScore: jest.fn().mockResolvedValue(0), + zCard: jest.fn().mockResolvedValue(0), + zRange: jest.fn().mockResolvedValue([]), + zAdd: jest.fn().mockResolvedValue(1), + pExpire: jest.fn().mockResolvedValue(true), + }; + }); +}); + +// Global mocks for BullMQ to prevent queue background workers from spinning up in tests +jest.mock("bullmq", () => { + return { + Queue: jest.fn().mockImplementation(() => ({ + add: jest.fn().mockResolvedValue({ id: "job-id" }), + on: jest.fn(), + })), + Worker: jest.fn().mockImplementation(() => ({ + on: jest.fn(), + })), + QueueScheduler: jest.fn().mockImplementation(() => ({ + on: jest.fn(), + })), + Job: jest.fn(), + }; +}); diff --git a/migrations/1745592000000_add-payout-randomization.ts b/migrations/1745592000000_add-payout-randomization.ts index daab064..95b90bd 100644 --- a/migrations/1745592000000_add-payout-randomization.ts +++ b/migrations/1745592000000_add-payout-randomization.ts @@ -1,33 +1,19 @@ -import { Kysely } from "kysely"; +import { MigrationBuilder } from "node-pg-migrate"; -export async function up(db: Kysely): Promise { +export async function up(pgm: MigrationBuilder): Promise { // Add payout_method and randomization_seed columns to circles table - await db.schema - .alterTable("circles") - .addColumn("payout_method", "varchar(20)", (col) => - col.notNull().defaultTo("fixed") - ) - .addColumn("randomization_seed", "varchar(255)") - .execute(); + pgm.addColumn("circles", { + payout_method: { type: "varchar(20)", notNull: true, default: "fixed" }, + randomization_seed: { type: "varchar(255)" }, + }); // Add updated_at column to members table - await db.schema - .alterTable("members") - .addColumn("updated_at", "timestamp", (col) => - col.notNull().defaultTo(db.fn("now")) - ) - .execute(); + pgm.addColumn("members", { + updated_at: { type: "timestamp", notNull: true, default: pgm.func("NOW()") }, + }); } -export async function down(db: Kysely): Promise { - await db.schema - .alterTable("circles") - .dropColumn("payout_method") - .dropColumn("randomization_seed") - .execute(); - - await db.schema - .alterTable("members") - .dropColumn("updated_at") - .execute(); +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropColumn("circles", ["payout_method", "randomization_seed"]); + pgm.dropColumn("members", ["updated_at"]); } diff --git a/migrations/1745600000000_add-private-circles.ts b/migrations/1745600000000_add-private-circles.ts index 29b63a8..5977a4a 100644 --- a/migrations/1745600000000_add-private-circles.ts +++ b/migrations/1745600000000_add-private-circles.ts @@ -24,14 +24,14 @@ export async function up(pgm: MigrationBuilder): Promise { notNull: true, default: "pending", check: "status IN ('pending', 'active', 'rejected', 'defaulted', 'completed')", - }); + } as any); // Make position nullable for pending members pgm.alterColumn("members", "position", { type: "integer", notNull: false, check: "position > 0", - }); + } as any); // Add reviewed_at column to track when creator approved/rejected pgm.addColumn("members", { @@ -51,7 +51,7 @@ export async function down(pgm: MigrationBuilder): Promise { type: "integer", notNull: true, check: "position > 0", - }); + } as any); // Revert members status check pgm.alterColumn("members", "status", { @@ -59,7 +59,7 @@ export async function down(pgm: MigrationBuilder): Promise { notNull: true, default: "pending", check: "status IN ('pending', 'active', 'defaulted', 'completed')", - }); + } as any); // Drop circle_type index pgm.dropIndex("circles", "circle_type", { diff --git a/migrations/1745900000000_members-db-persistence.ts b/migrations/1745900000000_members-db-persistence.ts index 9f0cfc9..d5030ad 100644 --- a/migrations/1745900000000_members-db-persistence.ts +++ b/migrations/1745900000000_members-db-persistence.ts @@ -15,7 +15,7 @@ export async function up(pgm: MigrationBuilder): Promise { notNull: true, default: "pending", check: "status IN ('pending','confirmed','missed','refund_pending')", - }); + } as any); // Add authorization_url column if not already present (used by Paystack flow) pgm.addColumn("contributions", { @@ -43,5 +43,5 @@ export async function down(pgm: MigrationBuilder): Promise { notNull: true, default: "pending", check: "status IN ('pending','confirmed','missed')", - }); + } as any); } diff --git a/migrations/1746000000000_add-refunded-contribution-status.ts b/migrations/1746000000000_add-refunded-contribution-status.ts index 872d3f6..246c2e9 100644 --- a/migrations/1746000000000_add-refunded-contribution-status.ts +++ b/migrations/1746000000000_add-refunded-contribution-status.ts @@ -15,7 +15,7 @@ export async function up(pgm: MigrationBuilder): Promise { notNull: true, default: "pending", check: "status IN ('pending','confirmed','missed','refund_pending','refunded')", - }); + } as any); // Add updated_at to contributions if not already present pgm.addColumn("contributions", { @@ -35,5 +35,5 @@ export async function down(pgm: MigrationBuilder): Promise { notNull: true, default: "pending", check: "status IN ('pending','confirmed','missed','refund_pending')", - }); + } as any); } diff --git a/migrations/1746800000000_add-audit-logs-table.ts b/migrations/1746800000000_add-audit-logs-table.ts index bc835bd..d0affbe 100644 --- a/migrations/1746800000000_add-audit-logs-table.ts +++ b/migrations/1746800000000_add-audit-logs-table.ts @@ -71,6 +71,6 @@ export async function up(pgm: MigrationBuilder): Promise { export async function down(pgm: MigrationBuilder): Promise { pgm.dropTrigger("audit_logs", "audit_logs_immutable"); - pgm.dropFunction("raise_immutable_error"); + pgm.dropFunction("raise_immutable_error", []); pgm.dropTable("audit_logs"); } diff --git a/package-lock.json b/package-lock.json index 6db1415..fd1d4c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,19 +14,27 @@ "@stellar/freighter-api": "3.1.0", "@stellar/stellar-sdk": "^12.3.0", "@tanstack/react-query": "^5.40.0", + "@vercel/analytics": "^1.1.0", + "@walletconnect/sign-client": "^2.23.9", "axios": "^1.7.2", + "bullmq": "^1.81.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "ioredis": "^5.3.2", "jose": "^5.6.3", "next": "14.2.4", "next-auth": "^4.24.7", "node-pg-migrate": "^8.0.4", "pino": "^9.3.2", "pino-pretty": "^11.2.1", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.52.1", "redis": "^5.12.1", + "resend": "^6.12.4", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", "zod": "^3.23.8" }, "devDependencies": { @@ -63,6 +71,12 @@ "dev": true, "license": "MIT" }, + "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/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -91,7 +105,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -739,6 +752,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1330,6 +1349,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1351,6 +1371,93 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1524,6 +1631,45 @@ "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.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1577,7 +1723,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1599,7 +1744,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" }, @@ -1636,7 +1780,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -2178,7 +2321,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -2224,7 +2366,6 @@ "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.44.1" }, @@ -2295,7 +2436,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -2399,7 +2539,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2455,6 +2594,81 @@ "dev": true, "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/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/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/@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/@scure/bip39/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/@sentry-internal/browser-utils": { "version": "8.55.1", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.1.tgz", @@ -2983,6 +3197,18 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@stellar/freighter-api": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-3.1.0.tgz", @@ -3203,7 +3429,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3259,11 +3486,21 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3274,6 +3511,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -3564,7 +3802,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -3587,7 +3826,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3636,7 +3874,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3648,7 +3885,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3682,6 +3918,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4109,70 +4354,938 @@ "win32" ] }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", + "node_modules/@walletconnect/core": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.9.tgz", + "integrity": "sha512-ws4WG8LeagUo2ERRo02HryXRcpwIRmCQ3pHLW5gWbVReLXXIpgk6ZAfID3fEGHevIwwnHSGww+nNhNpdXyiq0g==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/utils": "2.23.9", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.44.0", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "node_modules/@walletconnect/core/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "node_modules/@walletconnect/core/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/core/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@walletconnect/core/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/core/node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/environment/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/logger": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.2.tgz", + "integrity": "sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/logger/node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@walletconnect/logger/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/@walletconnect/logger/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/safe-json/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.9.tgz", + "integrity": "sha512-Xj+hw4E6mGRyhCdVOT/RMgnG+up/Y3v0ho5PlkVozvXWeVSqHNh9DmjLuU97a7OACoGd/oHBF6g3NVqD7MgCMQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.9", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/utils": "2.23.9", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/time/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/types": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.9.tgz", + "integrity": "sha512-IUl1PpD/Dig8IE2OZ9XtjbPohEyOZJ73xs92EDUzoIyzRtfm36g2D340pY3iu3AAdLv1yFiaZafB8Hf8RFze8A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/types/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/types/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/types/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@walletconnect/types/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/types/node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.9.tgz", + "integrity": "sha512-C5TltCs8UPypNiteYnKSv8+ZDK2EjVDyXCxN6kA9bkA+j6KGsNIV7l9MUA8WBAvE5Gi5EcBdhD3R9Hpo/1HHqQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/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/@walletconnect/utils/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@walletconnect/utils/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-getters/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", + "license": "MIT", + "dependencies": { + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-metadata/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { @@ -4180,6 +5293,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4188,13 +5302,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4211,6 +5327,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4224,6 +5341,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4236,6 +5354,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4250,6 +5369,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4259,13 +5379,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/abab": { "version": "2.0.6", @@ -4275,6 +5397,27 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abitype": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", + "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", + "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/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -4287,12 +5430,24 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4325,6 +5480,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -4389,6 +5545,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4406,6 +5563,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4421,7 +5579,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -4966,6 +6125,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.18", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", @@ -4999,6 +6167,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -5041,7 +6215,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5096,6 +6269,78 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bullmq": { + "version": "1.91.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.91.1.tgz", + "integrity": "sha512-u7dat9I8ZwouZ651AMZkBSvB6NVUPpnAjd4iokd9DM41whqIBnDjuL11h7+kEjcpiDKj6E+wxZiER00FqirZQg==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.6.0", + "get-port": "6.1.2", + "glob": "^8.0.3", + "ioredis": "^5.2.2", + "lodash": "^4.17.21", + "msgpackr": "^1.6.2", + "semver": "^7.3.7", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bullmq/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bullmq/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bullmq/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -5262,6 +6507,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -5551,6 +6797,29 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -5580,6 +6849,18 @@ "dev": true, "license": "MIT" }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5594,6 +6875,15 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -5821,6 +7111,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5830,6 +7126,15 @@ "node": ">=0.4.0" } }, + "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/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5840,6 +7145,28 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "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", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5901,7 +7228,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/domexception": { "version": "4.0.0", @@ -5985,11 +7313,55 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz", + "integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.5.tgz", + "integrity": "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" @@ -6153,7 +7525,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -6213,6 +7586,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6264,7 +7647,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6446,7 +7828,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6931,6 +8312,12 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -6945,7 +8332,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.20.1", @@ -7238,6 +8626,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", + "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -7336,7 +8736,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.1.0", @@ -7443,6 +8844,23 @@ "dev": true, "license": "MIT" }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -7640,6 +9058,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.4.tgz", + "integrity": "sha512-D/NzHWUmYJGXi++z67aMSrnisb9A3621CyRK5G89JyTlN13C8xf0g04DLxUKMufPem3e3L2JAXR6Z00OWy183Q==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7744,7 +9168,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7772,6 +9195,46 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.0.tgz", + "integrity": "sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.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/ioredis/node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9522,6 +10985,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -9876,6 +11345,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -9899,6 +11369,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10081,12 +11557,22 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10269,6 +11755,43 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -10310,11 +11833,21 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "14.2.4", @@ -10322,7 +11855,6 @@ "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.4", "@swc/helpers": "0.5.5", @@ -10458,6 +11990,12 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10480,6 +12018,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10487,6 +12040,12 @@ "dev": true, "license": "MIT" }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, "node_modules/node-pg-migrate": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz", @@ -10666,7 +12225,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10794,6 +12352,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, "node_modules/oidc-token-hash": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", @@ -10915,6 +12484,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "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.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/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/ox/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/ox/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/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -11105,13 +12737,15 @@ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": 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" + "license": "MIT", + "peer": true }, "node_modules/pg-int8": { "version": "1.0.1", @@ -11127,6 +12761,7 @@ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", "license": "MIT", + "peer": true, "peerDependencies": { "pg": ">=8.0" } @@ -11158,6 +12793,7 @@ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", "license": "MIT", + "peer": true, "dependencies": { "split2": "^4.1.0" } @@ -11371,7 +13007,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11391,6 +13026,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -11463,7 +13104,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -11519,6 +13159,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11534,6 +13175,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -11667,6 +13309,15 @@ ], "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/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -11701,6 +13352,12 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11715,7 +13372,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11728,7 +13384,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11742,7 +13397,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -11759,7 +13413,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-is-18": { "name": "react-is", @@ -11844,6 +13499,27 @@ "node": ">= 18.19.0" } }, + "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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -11915,6 +13591,7 @@ "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==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11961,6 +13638,27 @@ "dev": true, "license": "MIT" }, + "node_modules/resend": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.4.tgz", + "integrity": "sha512-lRpJ2Hxd+ht+JPDm97juRcUp9HOMuZyxaRFRFmc9Tx8iNWiei94Dx9v6SWufgKk2667C/uCeKKspMotOHSpCSg==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "standardwebhooks": "1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -12133,7 +13831,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -12287,6 +13984,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -12323,6 +14021,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12334,7 +14033,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/secure-json-parse": { "version": "2.7.0", @@ -12584,6 +14284,68 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/slow-redact": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", + "integrity": "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==", + "license": "MIT" + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz", + "integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.20.1" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sodium-native": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", @@ -12699,6 +14461,22 @@ "node": ">=8" } }, + "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/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -13077,6 +14855,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -13090,6 +14869,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -13108,6 +14888,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -13141,6 +14922,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -13155,6 +14937,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -13169,13 +14952,15 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -13275,7 +15060,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13370,7 +15154,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13574,9 +15357,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13585,6 +15367,21 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -13604,6 +15401,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -13755,6 +15558,15 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -13783,6 +15595,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -13806,6 +15619,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13868,6 +15682,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -13881,6 +15696,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -13890,6 +15706,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -14184,10 +16001,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14222,6 +16038,14 @@ "dev": true, "license": "MIT" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 39e856b..d380ab7 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,12 @@ "@stellar/freighter-api": "3.1.0", "@stellar/stellar-sdk": "^12.3.0", "@tanstack/react-query": "^5.40.0", + "@walletconnect/sign-client": "^2.23.9", "axios": "^1.7.2", + "bullmq": "^1.81.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "ioredis": "^5.3.2", "jose": "^5.6.3", "next": "14.2.4", "@vercel/analytics": "^1.1.0", @@ -59,10 +62,14 @@ "node-pg-migrate": "^8.0.4", "pino": "^9.3.2", "pino-pretty": "^11.2.1", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.52.1", "redis": "^5.12.1", + "resend": "^6.12.4", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", "zod": "^3.23.8" }, "devDependencies": { @@ -85,9 +92,7 @@ "lint-staged": "^15.2.7", "prettier": "^3.3.2", "ts-node": "^10.9.2", - "typescript": "^5.5.3", - "bullmq": "^1.81.0", - "ioredis": "^5.3.2" + "typescript": "^5.5.3" }, "lint-staged": { "*.{ts,tsx}": [ diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx index 738e36e..cf39da0 100644 --- a/src/app/analytics/page.tsx +++ b/src/app/analytics/page.tsx @@ -1,6 +1,6 @@ import { query } from "@/lib/db"; import type { Metadata } from "next"; -import type { PlatformStats } from "@/app/api/analytics/route"; +import type { PlatformStats } from "@/app/api/v1/analytics/route"; import styles from "./page.module.css"; export const metadata: Metadata = { diff --git a/src/app/api/admin/audit-logs/route.ts b/src/app/api/admin/audit-logs/route.ts deleted file mode 100644 index bcc872e..0000000 --- a/src/app/api/admin/audit-logs/route.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { withAdminAuth, withErrorHandler } from "@/server/middleware"; -import { getAuditLogs, getAuditLogCount } from "@/server/services/audit.service"; -import type { ApiResponse } from "@/types"; -import type { AuditLogResponse } from "@/server/services/audit.service"; - -interface AuditLogsListResponse { - logs: AuditLogResponse[]; - total: number; - limit: number; - offset: number; -} - -/** - * GET /api/admin/audit-logs - * Retrieves audit logs with optional filtering (admin only). - * - * Query Parameters: - * - actorId: Filter by admin user ID - * - action: Filter by action type (TRIGGER_PAYOUT, REMOVE_MEMBER, DELETE_USER, etc) - * - targetType: Filter by target type (CIRCLE, MEMBER, USER, PAYOUT) - * - targetId: Filter by target ID - * - startDate: Filter by start date (ISO 8601) - * - endDate: Filter by end date (ISO 8601) - * - limit: Number of results per page (default: 100, max: 1000) - * - offset: Pagination offset (default: 0) - * - * Response: - * { - * "success": true, - * "data": { - * "logs": [...], - * "total": 150, - * "limit": 100, - * "offset": 0 - * } - * } - */ -export const GET = withErrorHandler( - withAdminAuth(async (req: NextRequest) => { - const url = new URL(req.url); - const params = url.searchParams; - - // Parse query parameters - const actorId = params.get("actorId") || undefined; - const action = (params.get("action") as any) || undefined; - const targetType = (params.get("targetType") as any) || undefined; - const targetId = params.get("targetId") || undefined; - const startDateStr = params.get("startDate"); - const endDateStr = params.get("endDate"); - const limit = Math.min(parseInt(params.get("limit") || "100"), 1000); - const offset = parseInt(params.get("offset") || "0"); - - const startDate = startDateStr ? new Date(startDateStr) : undefined; - const endDate = endDateStr ? new Date(endDateStr) : undefined; - - // Validate dates - if (startDate && isNaN(startDate.getTime())) { - return NextResponse.json>( - { success: false, error: "Invalid startDate format. Use ISO 8601 (e.g., 2024-01-01T00:00:00Z)" }, - { status: 400 } - ); - } - - if (endDate && isNaN(endDate.getTime())) { - return NextResponse.json>( - { success: false, error: "Invalid endDate format. Use ISO 8601 (e.g., 2024-01-01T00:00:00Z)" }, - { status: 400 } - ); - } - - // Fetch logs and count - const logs = await getAuditLogs({ - actorId, - action, - targetType, - targetId, - startDate, - endDate, - limit, - offset, - }); - - const total = await getAuditLogCount({ - actorId, - action, - targetType, - targetId, - startDate, - endDate, - }); - - return NextResponse.json>( - { - success: true, - data: { - logs, - total, - limit, - offset, - }, - }, - { status: 200 } - ); - }) -); diff --git a/src/app/api/admin/circles/[id]/payout/route.ts b/src/app/api/admin/circles/[id]/payout/route.ts deleted file mode 100644 index ffe776d..0000000 --- a/src/app/api/admin/circles/[id]/payout/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { getCircleById, getMembersByCircle } from "@/server/services/circle.service"; -import { processCyclePayout, PayoutLockError } from "@/server/services/payout.service"; -import { logAuditAction } from "@/server/services/audit.service"; -import { withAdminAuth, withErrorHandler } from "@/server/middleware"; -import { getRequestContext } from "@/lib/request-context"; -import { authOptions } from "@/lib/auth"; -import type { ApiResponse, Payout } from "@/types"; - -/** - * POST /api/admin/circles/[id]/payout - * Manually trigger a payout cycle for a circle (admin only). - * Requires the circle to be active and the next recipient to have a Stellar key. - * - * Logs: TRIGGER_PAYOUT action to audit trail - */ -export const POST = withErrorHandler( - withAdminAuth(async (req: NextRequest, ctx: unknown) => { - const { params } = ctx as { params: { id: string } }; - const session = await getServerSession(authOptions); - const actorId = (session?.user as { id?: string })?.id; - - const circle = await getCircleById(params.id); - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - if (circle.status !== "active") { - return NextResponse.json>( - { success: false, error: "Circle is not active" }, - { status: 400 } - ); - } - - const members = await getMembersByCircle(params.id); - const recipient = members.find((m) => m.position === circle.currentCycle); - if (!recipient) { - return NextResponse.json>( - { success: false, error: "No recipient found for current cycle" }, - { status: 400 } - ); - } - - try { - // recipientStellarKey is resolved inside processCyclePayout via the contract path, - // or passed here for the Horizon fallback. We pass empty string for contract circles. - const payout = await processCyclePayout(params.id, ""); - - // Log the audit action - if (actorId) { - const requestContext = getRequestContext(req); - await logAuditAction(actorId, "TRIGGER_PAYOUT", "PAYOUT", params.id, { - details: { - circleName: circle.name, - recipientMemberId: recipient.id, - cycle: circle.currentCycle, - amountUsdc: payout.amountUsdc, - txHash: payout.txHash, - }, - ...requestContext, - }); - } - - return NextResponse.json>({ success: true, data: payout }); - } catch (err) { - if (err instanceof PayoutLockError) { - return NextResponse.json>( - { success: false, error: "Payout already in progress for this circle" }, - { status: 409 } - ); - } - throw err; - } - }) -); diff --git a/src/app/api/admin/disputes/route.ts b/src/app/api/admin/disputes/route.ts deleted file mode 100644 index 073f275..0000000 --- a/src/app/api/admin/disputes/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { withErrorHandler } from "@/server/middleware"; -import { resolveDispute, confirmContributionFromDispute } from "@/server/services/dispute.service"; -import type { ApiResponse, Dispute } from "@/types"; -import { z } from "zod"; - -const ResolveDisputeSchema = z.object({ - disputeId: z.string().uuid(), - status: z.enum(["resolved", "rejected"]), - resolutionNotes: z.string().min(5).max(500), - txHash: z.string().optional(), - contributionId: z.string().uuid().optional(), -}); - -export const POST = withErrorHandler(async (req: NextRequest) => { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const user = session.user as { role?: string }; - if (user.role !== "admin") { - return NextResponse.json>( - { success: false, error: "Admin access required" }, - { status: 403 } - ); - } - - const body = await req.json(); - const parsed = ResolveDisputeSchema.parse(body); - - const dispute = await resolveDispute( - parsed.disputeId, - parsed.status, - parsed.resolutionNotes, - session.user.id - ); - - // If resolving as confirmed, update the contribution - if (parsed.status === "resolved" && parsed.txHash && parsed.contributionId) { - await confirmContributionFromDispute(parsed.disputeId, parsed.contributionId, parsed.txHash); - } - - return NextResponse.json>( - { success: true, data: dispute }, - { status: 200 } - ); -}); diff --git a/src/app/api/admin/horizon-stream/route.ts b/src/app/api/admin/horizon-stream/route.ts deleted file mode 100644 index 182817c..0000000 --- a/src/app/api/admin/horizon-stream/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { - startHorizonStream, - stopHorizonStream, - getStreamStatus, -} from "@/server/services/horizon-stream.service"; -import { withErrorHandler } from "@/server/middleware"; -import type { ApiResponse } from "@/types"; - -/** - * GET /api/admin/horizon-stream - * Get Horizon stream status - */ -export const GET = withErrorHandler(async (_req: NextRequest) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - // TODO: Add admin role check - // if (session.user.role !== 'admin') { - // return NextResponse.json>( - // { success: false, error: "Forbidden" }, - // { status: 403 } - // ); - // } - - const status = getStreamStatus(); - - return NextResponse.json>({ - success: true, - data: status, - }); -}); - -/** - * POST /api/admin/horizon-stream - * Start or stop the Horizon stream - */ -export const POST = withErrorHandler(async (req: NextRequest) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - // TODO: Add admin role check - - const body = await req.json(); - const { action } = body as { action: "start" | "stop" }; - - if (action === "start") { - await startHorizonStream(); - return NextResponse.json>({ - success: true, - data: { message: "Horizon stream started" }, - }); - } else if (action === "stop") { - stopHorizonStream(); - return NextResponse.json>({ - success: true, - data: { message: "Horizon stream stopped" }, - }); - } else { - return NextResponse.json>( - { success: false, error: "Invalid action. Use 'start' or 'stop'" }, - { status: 400 } - ); - } -}); diff --git a/src/app/api/admin/payouts/route.ts b/src/app/api/admin/payouts/route.ts deleted file mode 100644 index 8cea588..0000000 --- a/src/app/api/admin/payouts/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextResponse } from "next/server"; -import { adminListPayouts } from "@/server/services/admin.service"; -import { withAdminAuth, withErrorHandler } from "@/server/middleware"; -import type { ApiResponse } from "@/types"; -import type { AdminPayoutRow } from "@/server/services/admin.service"; - -export const GET = withErrorHandler( - withAdminAuth(async () => { - const payouts = await adminListPayouts(); - return NextResponse.json>({ success: true, data: payouts }); - }) -); diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts deleted file mode 100644 index 2df534f..0000000 --- a/src/app/api/auth/logout/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { NextResponse } from "next/server"; -import { revokeAllUserTokens } from "@/lib/refresh-tokens"; -import type { ApiResponse } from "@/types"; - -/** - * POST /api/auth/logout - * Invalidates the session server-side by revoking all refresh tokens. - * Clears the refresh token cookie. - */ -export async function POST() { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Not authenticated" }, - { status: 401 } - ); - } - - const userId = (session.user as { id?: string }).id; - if (!userId) { - return NextResponse.json>( - { success: false, error: "User ID not found in session" }, - { status: 401 } - ); - } - - // Revoke all refresh tokens for this user - await revokeAllUserTokens(userId); - - // Clear the refresh token cookie - const response = NextResponse.json>( - { success: true, data: { message: "Logged out successfully" } }, - { status: 200 } - ); - - response.cookies.set("refreshToken", "", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 0, - path: "/", - }); - - return response; -} diff --git a/src/app/api/auth/refresh/route.ts b/src/app/api/auth/refresh/route.ts deleted file mode 100644 index a81554e..0000000 --- a/src/app/api/auth/refresh/route.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { withErrorHandler } from "@/server/middleware"; -import { verifyRefreshToken, revokeRefreshToken, generateRefreshToken, getTokenExpiries } from "@/lib/refresh-tokens"; -import { query } from "@/lib/db"; -import { SignJWT } from "jose"; -import { serverConfig } from "@/server/config"; -import type { ApiResponse } from "@/types"; - -const SECRET = new TextEncoder().encode(serverConfig.authSecret); - -/** - * POST /api/auth/refresh - * Exchanges a refresh token for a new access token and refresh token. - * - * Request body: - * { - * "refreshToken": "token_string" - * } - * - * Response: - * { - * "success": true, - * "data": { - * "accessToken": "new_jwt_token", - * "refreshToken": "new_refresh_token", - * "expiresIn": 900 - * } - * } - */ -export const POST = withErrorHandler(async (req: NextRequest) => { - try { - const body = await req.json(); - const refreshToken = body.refreshToken as string; - - if (!refreshToken) { - return NextResponse.json>( - { success: false, error: "Refresh token is required" }, - { status: 400 } - ); - } - - // Verify the refresh token and get the user ID - const userId = await verifyRefreshToken(refreshToken); - if (!userId) { - return NextResponse.json>( - { success: false, error: "Invalid or expired refresh token" }, - { status: 401 } - ); - } - - // Get user details - const userResult = await query<{ id: string; phone: string; display_name: string; role: string }>( - "SELECT id, phone, display_name, role FROM users WHERE id = $1", - [userId] - ); - - const user = userResult.rows[0]; - if (!user) { - return NextResponse.json>( - { success: false, error: "User not found" }, - { status: 401 } - ); - } - - // Revoke the old refresh token - await revokeRefreshToken(refreshToken); - - // Generate new refresh token - const newRefreshToken = await generateRefreshToken(userId); - - // Get token expiries - const expiries = getTokenExpiries(); - - // Create new JWT access token - const now = Math.floor(Date.now() / 1000); - const accessToken = await new SignJWT({ - id: user.id, - phone: user.phone, - role: user.role, - accessTokenExpires: expiries.accessTokenExpires, - refreshTokenExpires: expiries.refreshTokenExpires, - }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt(now) - .setExpirationTime(now + 15 * 60) // 15 minutes - .sign(SECRET); - - // Create response with httpOnly cookie for refresh token - const response = NextResponse.json>( - { - success: true, - data: { - accessToken, - expiresIn: 15 * 60, // 15 minutes in seconds - }, - }, - { status: 200 } - ); - - // Set refresh token in httpOnly cookie - response.cookies.set("refreshToken", newRefreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 7 * 24 * 60 * 60, // 7 days in seconds - path: "/", - }); - - return response; - } catch (error) { - console.error("[refresh] Error:", error); - return NextResponse.json>( - { success: false, error: "Failed to refresh token" }, - { status: 500 } - ); - } -}); diff --git a/src/app/api/auth/send-otp/route.ts b/src/app/api/auth/send-otp/route.ts deleted file mode 100644 index db94879..0000000 --- a/src/app/api/auth/send-otp/route.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { sendOtp } from "@/lib/sms"; -import { withRateLimit, withErrorHandler } from "@/server/middleware"; -import { sendOtpSchema } from "@/types/schemas"; -import type { ApiResponse } from "@/types"; -import { getRedis } from "@/lib/redis"; -import { getLockoutStatus } from "@/lib/lockout"; - -interface SendOtpResponse { - message: string; - lockout?: { - isLocked: boolean; - attempts: number; - remainingAttempts: number; - lockoutExpiresAt?: number; - lockoutRemainingSeconds?: number; - }; -} - -/** - * POST /api/auth/send-otp - * Sends an OTP to a phone number with brute-force protection. - * - * Acceptance Criteria: - * ✅ Max 5 OTP attempts per phone number per 10 minutes - * ✅ Account locked for 30 minutes after 5 failures - * ✅ Lockout status returned in API response - * ✅ Attempts tracked in Redis - * - * Request body: - * { - * "phone": "+234..." - * } - * - * Success Response (200): - * { - * "success": true, - * "data": { - * "message": "OTP sent successfully" - * } - * } - * - * Locked Response (423): - * { - * "success": false, - * "error": "Account locked due to too many failed attempts. Please try again in 30 minutes.", - * "data": { - * "lockout": { - * "isLocked": true, - * "attempts": 5, - * "remainingAttempts": 0, - * "lockoutExpiresAt": 1234567890, - * "lockoutRemainingSeconds": 1800 - * } - * } - * } - */ -export const POST = withRateLimit( - withErrorHandler(async (req: NextRequest) => { - const body = await req.json(); - const parsed = sendOtpSchema.safeParse(body); - if (!parsed.success) { - return NextResponse.json>( - { success: false, error: parsed.error.errors[0].message }, - { status: 400 } - ); - } - - const { phone } = parsed.data; - - // Check for account lockout (brute-force protection) - const lockoutStatus = await getLockoutStatus(phone); - if (lockoutStatus.isLocked) { - return NextResponse.json>( - { - success: false, - error: "Account locked due to too many failed attempts. Please try again in 30 minutes.", - data: { - message: "Account is locked", - lockout: lockoutStatus, - } as any, - }, - { status: 423 } - ); - } - - const otp = await sendOtp(phone); - - // Store OTP in Redis with 10-minute expiry - const redis = await getRedis(); - await redis.set(`otp:${phone}`, otp, { EX: 600 }); - - if (process.env.NODE_ENV === "development") console.warn(`[DEV] OTP for ${phone}: ${otp}`); - - return NextResponse.json>({ - success: true, - data: { message: "OTP sent successfully" }, - }); - }), - { limit: 5, windowMs: 10 * 60 * 1000 } -); diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts deleted file mode 100644 index b84a7a5..0000000 --- a/src/app/api/auth/verify-otp/route.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { withErrorHandler } from "@/server/middleware"; -import { verifyOtpSchema } from "@/types/schemas"; -import { getRedis } from "@/lib/redis"; -import { getLockoutStatus, recordFailure, resetLockout } from "@/lib/lockout"; -import { query } from "@/lib/db"; -import type { ApiResponse } from "@/types"; - -interface VerifyOtpResponse { - message: string; - user?: { - id: string; - phone: string; - displayName: string; - role: string; - }; - lockout?: { - isLocked: boolean; - attempts: number; - remainingAttempts: number; - lockoutExpiresAt?: number; - lockoutRemainingSeconds?: number; - }; -} - -/** - * POST /api/auth/verify-otp - * Verifies an OTP and returns lockout status on failure. - * - * Request body: - * { - * "phone": "+234...", - * "otp": "123456" - * } - * - * Success Response (200): - * { - * "success": true, - * "data": { - * "message": "OTP verified successfully", - * "user": { - * "id": "user-uuid", - * "phone": "+234...", - * "displayName": "User Name", - * "role": "user" - * } - * } - * } - * - * Failure Response (401): - * { - * "success": false, - * "error": "Invalid or expired OTP", - * "data": { - * "lockout": { - * "isLocked": false, - * "attempts": 3, - * "remainingAttempts": 2 - * } - * } - * } - * - * Locked Response (423): - * { - * "success": false, - * "error": "Account locked due to too many failed attempts", - * "data": { - * "lockout": { - * "isLocked": true, - * "attempts": 5, - * "remainingAttempts": 0, - * "lockoutExpiresAt": 1234567890, - * "lockoutRemainingSeconds": 1800 - * } - * } - * } - */ -export const POST = withErrorHandler(async (req: NextRequest) => { - try { - const body = await req.json(); - const parsed = verifyOtpSchema.safeParse(body); - - if (!parsed.success) { - return NextResponse.json>( - { - success: false, - error: parsed.error.errors[0].message, - }, - { status: 400 } - ); - } - - const { phone, otp } = parsed.data; - - // Check for account lockout - const lockoutStatus = await getLockoutStatus(phone); - if (lockoutStatus.isLocked) { - return NextResponse.json>( - { - success: false, - error: "Account locked due to too many failed attempts. Please try again in 30 minutes.", - data: { - message: "Account is locked", - lockout: lockoutStatus, - } as any, - }, - { status: 423 } - ); - } - - // Verify OTP from Redis - const redis = await getRedis(); - const storedOtp = await redis.get(`otp:${phone}`); - - if (!storedOtp || storedOtp !== otp) { - // Record failure and get updated status - const updatedStatus = await recordFailure(phone); - - // If newly locked, return 423; otherwise 401 - const statusCode = updatedStatus.isLocked ? 423 : 401; - const errorMessage = updatedStatus.isLocked - ? "Account locked due to too many failed attempts. Please try again in 30 minutes." - : "Invalid or expired OTP. Please try again."; - - return NextResponse.json>( - { - success: false, - error: errorMessage, - data: { - message: "OTP verification failed", - lockout: updatedStatus, - } as any, - }, - { status: statusCode } - ); - } - - // OTP is valid - reset failure tracking and delete OTP - await resetLockout(phone); - await redis.del(`otp:${phone}`); - - // Load or create user - let user = await query<{ id: string; phone: string; display_name: string; role: string }>( - "SELECT id, phone, display_name, role FROM users WHERE phone = $1", - [phone] - ); - - if (user.rows.length === 0) { - // Create user on first successful OTP verification - const result = await query<{ id: string; phone: string; display_name: string; role: string }>( - `INSERT INTO users (id, phone, display_name, role, reputation_score, created_at) - VALUES (gen_random_uuid(), $1, 'Ajosave User', 'user', 0, NOW()) - ON CONFLICT (phone) DO UPDATE SET phone = EXCLUDED.phone - RETURNING id, phone, display_name, role`, - [phone] - ); - user = result; - } - - const userData = user.rows[0]; - - return NextResponse.json>( - { - success: true, - data: { - message: "OTP verified successfully", - user: { - id: userData.id, - phone: userData.phone, - displayName: userData.display_name, - role: userData.role, - }, - }, - }, - { status: 200 } - ); - } catch (error) { - console.error("[verify-otp] Error:", error); - return NextResponse.json>( - { - success: false, - error: "Failed to verify OTP", - }, - { status: 500 } - ); - } -}); diff --git a/src/app/api/circles/[id]/contribute/__tests__/route.test.ts b/src/app/api/circles/[id]/contribute/__tests__/route.test.ts deleted file mode 100644 index 509e8ec..0000000 --- a/src/app/api/circles/[id]/contribute/__tests__/route.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @jest-environment node - */ -import { POST } from "@/app/api/circles/[id]/contribute/route"; -import { NextRequest } from "next/server"; - -jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); -jest.mock("@/lib/auth", () => ({ authOptions: {} })); -jest.mock("@/server/services/circle.service"); -jest.mock("@/lib/paystack"); -jest.mock("@/lib/db"); -jest.mock("@/server/config", () => ({ - serverConfig: { - app: { url: "http://localhost:3000" }, - paystack: { secretKey: "test" }, - stellar: { network: "testnet", sorobanRpcUrl: "http://localhost", ajoContractId: "test" }, - }, -})); -jest.mock("@/server/middleware", () => ({ - withErrorHandler: (fn: Function) => fn, -})); - -import { getServerSession } from "next-auth"; -import { getCircleById, getMembersByCircle } from "@/server/services/circle.service"; -import { initializePayment } from "@/lib/paystack"; -import * as db from "@/lib/db"; - -const mockSession = getServerSession as jest.MockedFunction; -const mockGetCircle = getCircleById as jest.MockedFunction; -const mockGetMembers = getMembersByCircle as jest.MockedFunction; -const mockInitPayment = initializePayment as jest.MockedFunction; -const mockQuery = db.query as jest.MockedFunction; - -const CIRCLE_ID = "circle-1"; -const MEMBER_ID = "member-1"; -const USER_ID = "user-1"; -const CYCLE = 2; - -const circle = { - id: CIRCLE_ID, - status: "active", - currentCycle: CYCLE, - contributionFiat: 5000, - contributionCurrency: "NGN", - contributionUsdc: "3.0000000", -} as any; - -const member = { id: MEMBER_ID, userId: USER_ID } as any; - -function makeRequest() { - return new NextRequest(`http://localhost/api/circles/${CIRCLE_ID}/contribute`, { method: "POST" }); -} - -beforeEach(() => { - jest.clearAllMocks(); - mockSession.mockResolvedValue({ user: { id: USER_ID, email: "user@test.com" } } as any); - mockGetCircle.mockResolvedValue(circle); - mockGetMembers.mockResolvedValue([member]); -}); - -describe("POST /api/circles/[id]/contribute", () => { - it("returns 401 when unauthenticated", async () => { - mockSession.mockResolvedValue(null); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(401); - }); - - it("returns 404 when circle not found", async () => { - mockGetCircle.mockResolvedValue(null); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(404); - }); - - it("returns 400 when circle is not active", async () => { - mockGetCircle.mockResolvedValue({ ...circle, status: "open" } as any); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(400); - }); - - it("returns 403 when user is not a member", async () => { - mockGetMembers.mockResolvedValue([]); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(403); - }); - - it("returns existing authorizationUrl on duplicate (idempotent)", async () => { - const existingUrl = "https://paystack.com/pay/existing"; - mockQuery.mockResolvedValueOnce({ - rows: [{ paystack_reference: `ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}`, authorization_url: existingUrl }], - rowCount: 1, - } as any); - - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - const json = await res.json(); - - expect(res.status).toBe(200); - expect(json.success).toBe(true); - expect(json.data.authorizationUrl).toBe(existingUrl); - expect(json.data.reference).toBe(`ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}`); - expect(mockInitPayment).not.toHaveBeenCalled(); - }); - - it("initializes payment and upserts contribution for new request", async () => { - const authUrl = "https://paystack.com/pay/new"; - // No existing pending contribution - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); - // Upsert insert - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); - mockInitPayment.mockResolvedValue({ authorizationUrl: authUrl, reference: `ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}` }); - - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - const json = await res.json(); - - expect(res.status).toBe(200); - expect(json.success).toBe(true); - expect(json.data.authorizationUrl).toBe(authUrl); - expect(json.data.reference).toBe(`ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}`); - - expect(mockInitPayment).toHaveBeenCalledWith( - expect.objectContaining({ reference: `ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}` }) - ); - // Upsert query - expect(mockQuery).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("ON CONFLICT (member_id, cycle_number)"), - expect.arrayContaining([`ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}`, authUrl]) - ); - }); -}); diff --git a/src/app/api/circles/[id]/contribute/route.ts b/src/app/api/circles/[id]/contribute/route.ts deleted file mode 100644 index d73d9ab..0000000 --- a/src/app/api/circles/[id]/contribute/route.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getCircleById, getMembersByCircle } from "@/server/services/circle.service"; -import { initializePayment } from "@/lib/paystack"; -import { serverConfig } from "@/server/config"; -import { withErrorHandler } from "@/server/middleware"; -import { query } from "@/lib/db"; -import { randomUUID } from "crypto"; -import type { ApiResponse } from "@/types"; - -export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const circle = await getCircleById(params.id); - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - if (circle.status !== "active") { - return NextResponse.json>( - { success: false, error: "Circle is not active" }, - { status: 400 } - ); - } - - const circleMembers = await getMembersByCircle(params.id); - const userId = (session.user as { id: string; email?: string }).id; - const member = circleMembers.find((m) => m.userId === userId); - if (!member) { - return NextResponse.json>( - { success: false, error: "You are not a member of this circle" }, - { status: 403 } - ); - } - - // Deterministic reference: ajo-{circleId}-{memberId}-{cycleNumber} - const reference = `ajo-${params.id}-${member.id}-${circle.currentCycle}`; - - // Return existing authorizationUrl if a pending contribution already exists for this cycle - const { rows: existing } = await query<{ paystack_reference: string; authorization_url: string }>( - `SELECT paystack_reference, authorization_url - FROM contributions - WHERE member_id = $1 AND cycle_number = $2 AND status = 'pending' - LIMIT 1`, - [member.id, circle.currentCycle] - ); - if (existing.length > 0 && existing[0].authorization_url) { - return NextResponse.json>({ - success: true, - data: { authorizationUrl: existing[0].authorization_url, reference: existing[0].paystack_reference }, - }); - } - - const callbackUrl = `${serverConfig.app.url}/circles/${params.id}/contribute/callback?reference=${reference}`; - - const { authorizationUrl, platformFee } = await initializePayment({ - email: (session.user as { email?: string }).email ?? `${userId}@ajosave.app`, - amount: circle.contributionFiat, - currency: circle.contributionCurrency, - reference, - callbackUrl, - metadata: { - circleId: params.id, - memberId: member.id, - cycleNumber: circle.currentCycle, - }, - }); - - // Upsert pending contribution with paystack_reference and authorization_url - await query( - `INSERT INTO contributions (id, circle_id, member_id, cycle_number, amount_usdc, status, paystack_reference, authorization_url) - VALUES ($1, $2, $3, $4, $5, 'pending', $6, $7) - ON CONFLICT (member_id, cycle_number) DO UPDATE - SET paystack_reference = EXCLUDED.paystack_reference, - authorization_url = EXCLUDED.authorization_url`, - [randomUUID(), params.id, member.id, circle.currentCycle, circle.contributionUsdc, reference, authorizationUrl] - ); - - return NextResponse.json>({ - success: true, - data: { authorizationUrl, reference, platformFee }, - }); -}); diff --git a/src/app/api/circles/[id]/contribute/verify/__tests__/route.test.ts b/src/app/api/circles/[id]/contribute/verify/__tests__/route.test.ts deleted file mode 100644 index a653429..0000000 --- a/src/app/api/circles/[id]/contribute/verify/__tests__/route.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @jest-environment node - */ -import { GET } from "@/app/api/circles/[id]/contribute/verify/route"; -import { NextRequest } from "next/server"; - -jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); -jest.mock("@/lib/auth", () => ({ authOptions: {} })); -jest.mock("@/lib/paystack"); -jest.mock("@/server/services/circle.service"); -jest.mock("@/server/services/notification.service"); -jest.mock("@/server/middleware", () => ({ - withErrorHandler: (fn: Function) => fn, -})); -jest.mock("@/server/config", () => ({ - serverConfig: { paystack: { secretKey: "test" }, stellar: { network: "testnet", sorobanRpcUrl: "http://localhost", ajoContractId: "test" } }, -})); - -import { getServerSession } from "next-auth"; -import { verifyPayment } from "@/lib/paystack"; -import { getCircleById } from "@/server/services/circle.service"; -import { notifyContributionReceived } from "@/server/services/notification.service"; - -const mockSession = getServerSession as jest.MockedFunction; -const mockVerify = verifyPayment as jest.MockedFunction; -const mockGetCircle = getCircleById as jest.MockedFunction; -const mockNotify = notifyContributionReceived as jest.MockedFunction; - -function makeRequest(params: Record) { - const url = new URL("http://localhost/api/circles/c1/contribute/verify"); - Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); - return new NextRequest(url); -} - -beforeEach(() => { - jest.clearAllMocks(); - mockNotify.mockResolvedValue(undefined as any); -}); - -describe("GET /api/circles/[id]/contribute/verify", () => { - it("returns 400 when reference is missing", async () => { - const res = await GET(makeRequest({})); - expect(res.status).toBe(400); - const json = await res.json(); - expect(json.success).toBe(false); - }); - - it("returns verified payment data for a successful payment", async () => { - mockVerify.mockResolvedValue({ status: "success", amount: 500000, currency: "NGN" }); - mockSession.mockResolvedValue({ user: { id: "user-1" } } as any); - mockGetCircle.mockResolvedValue({ name: "Test Circle", contributionUsdc: "3.0" } as any); - - const res = await GET(makeRequest({ reference: "ajo-c1-m1-2", circleId: "c1", cycleNumber: "2" })); - const json = await res.json(); - - expect(res.status).toBe(200); - expect(json.success).toBe(true); - expect(json.data.status).toBe("success"); - expect(json.data.amount).toBe(500000); - }); - - it("sends notification on successful payment", async () => { - mockVerify.mockResolvedValue({ status: "success", amount: 500000, currency: "NGN" }); - mockSession.mockResolvedValue({ user: { id: "user-1" } } as any); - mockGetCircle.mockResolvedValue({ name: "Test Circle", contributionUsdc: "3.0" } as any); - - await GET(makeRequest({ reference: "ajo-c1-m1-2", circleId: "c1", cycleNumber: "2" })); - - // Allow async notification to fire - await new Promise((r) => setTimeout(r, 0)); - expect(mockNotify).toHaveBeenCalledWith("user-1", "Test Circle", "3.0", 2); - }); - - it("returns failed status without sending notification", async () => { - mockVerify.mockResolvedValue({ status: "failed", amount: 500000, currency: "NGN" }); - mockSession.mockResolvedValue({ user: { id: "user-1" } } as any); - - const res = await GET(makeRequest({ reference: "ref-failed", circleId: "c1", cycleNumber: "2" })); - const json = await res.json(); - - expect(res.status).toBe(200); - expect(json.data.status).toBe("failed"); - expect(mockNotify).not.toHaveBeenCalled(); - }); - - it("returns pending status", async () => { - mockVerify.mockResolvedValue({ status: "pending", amount: 0, currency: "NGN" }); - - const res = await GET(makeRequest({ reference: "ref-pending" })); - const json = await res.json(); - - expect(json.data.status).toBe("pending"); - }); - - it("propagates Paystack API errors via error handler", async () => { - mockVerify.mockRejectedValue(new Error("Paystack down")); - await expect(GET(makeRequest({ reference: "ref-x" }))).rejects.toThrow("Paystack down"); - }); -}); diff --git a/src/app/api/circles/[id]/contribute/verify/route.ts b/src/app/api/circles/[id]/contribute/verify/route.ts deleted file mode 100644 index 36dca20..0000000 --- a/src/app/api/circles/[id]/contribute/verify/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { verifyPayment } from "@/lib/paystack"; -import { withErrorHandler } from "@/server/middleware"; -import { notifyContributionReceived } from "@/server/services/notification.service"; -import { getCircleById } from "@/server/services/circle.service"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import type { ApiResponse } from "@/types"; - -export const GET = withErrorHandler(async (req: NextRequest) => { - const reference = req.nextUrl.searchParams.get("reference"); - if (!reference) { - return NextResponse.json>( - { success: false, error: "Missing reference" }, - { status: 400 } - ); - } - - const result = await verifyPayment(reference); - - // Send SMS confirmation if payment was successful - if (result.status === "success") { - const session = await getServerSession(authOptions); - const circleId = req.nextUrl.searchParams.get("circleId"); - const cycleNumber = req.nextUrl.searchParams.get("cycleNumber"); - - if (session?.user?.id && circleId && cycleNumber) { - const circle = await getCircleById(circleId); - if (circle) { - // Send notification (async, don't block) - notifyContributionReceived( - session.user.id, - circle.name, - circle.contributionUsdc, - parseInt(cycleNumber) - ).catch(err => { - console.error("Failed to send contribution confirmation:", err); - }); - } - } - } - - return NextResponse.json>({ success: true, data: result }); -}); diff --git a/src/app/api/circles/[id]/invite/route.ts b/src/app/api/circles/[id]/invite/route.ts deleted file mode 100644 index 5f099da..0000000 --- a/src/app/api/circles/[id]/invite/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getCircleById } from "@/server/services/circle.service"; -import { createInviteToken } from "@/lib/tokens"; -import { withErrorHandler } from "@/server/middleware"; -import { serverConfig } from "@/server/config"; -import type { ApiResponse } from "@/types"; - -export const GET = withErrorHandler(async (req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const circle = await getCircleById(params.id); - - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - - const userId = (session.user as { id: string }).id; - if (circle.creatorId !== userId) { - return NextResponse.json>( - { success: false, error: "Only the circle creator can generate invite links" }, - { status: 403 } - ); - } - - const token = await createInviteToken(circle.id); - const inviteUrl = `${serverConfig.app.url}/circles/${circle.id}/join?token=${token}`; - - return NextResponse.json>({ - success: true, - data: { inviteUrl }, - }); -}); diff --git a/src/app/api/circles/[id]/join/__tests__/route.test.ts b/src/app/api/circles/[id]/join/__tests__/route.test.ts deleted file mode 100644 index c121b77..0000000 --- a/src/app/api/circles/[id]/join/__tests__/route.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @jest-environment node - */ -import { POST } from "@/app/api/circles/[id]/join/route"; -import { NextRequest } from "next/server"; -import { getServerSession } from "next-auth"; -import { getCircleById, joinCircle } from "@/server/services/circle.service"; -import { verifyInviteToken } from "@/lib/tokens"; - -jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); -jest.mock("@/lib/auth", () => ({ authOptions: {} })); -jest.mock("@/server/services/circle.service"); -jest.mock("@/lib/tokens"); -jest.mock("@/server/middleware", () => ({ - withErrorHandler: (fn: Function) => fn, -})); - -const mockSession = getServerSession as jest.MockedFunction; -const mockGetCircle = getCircleById as jest.MockedFunction; -const mockJoinCircle = joinCircle as jest.MockedFunction; -const mockVerifyToken = verifyInviteToken as jest.MockedFunction; - -const USER_ID = "user-1"; -const CIRCLE_ID = "circle-1"; - -describe("POST /api/circles/[id]/join", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const makeRequest = (body: any = {}) => { - return new NextRequest(`http://localhost/api/circles/${CIRCLE_ID}/join`, { - method: "POST", - body: JSON.stringify({ - stellarPublicKey: "GDZ74K6L3R5S6N4X3P6Q5W4E3R2T1Y0U9I8O7P6A5S4D3F2G1H0J9K8L", - ...body, - }), - }); - }; - - it("joins a public circle successfully", async () => { - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - mockGetCircle.mockResolvedValue({ id: CIRCLE_ID, circleType: "public" } as any); - const mockMember = { id: "member-1", circleId: CIRCLE_ID, userId: USER_ID }; - mockJoinCircle.mockResolvedValue(mockMember as any); - - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - const json = await res.json(); - - expect(res.status).toBe(201); - expect(json.success).toBe(true); - expect(json.data).toEqual(mockMember); - expect(mockJoinCircle).toHaveBeenCalledWith(CIRCLE_ID, USER_ID, false); - }); - - it("joins a private circle successfully with valid token", async () => { - const token = "valid-token"; - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - mockGetCircle.mockResolvedValue({ id: CIRCLE_ID, circleType: "private" } as any); - mockVerifyToken.mockResolvedValue({ circleId: CIRCLE_ID } as any); - const mockMember = { id: "member-1", circleId: CIRCLE_ID, userId: USER_ID }; - mockJoinCircle.mockResolvedValue(mockMember as any); - - const res = await POST(makeRequest({ token }), { params: { id: CIRCLE_ID } }); - const json = await res.json(); - - expect(res.status).toBe(201); - expect(json.success).toBe(true); - expect(mockJoinCircle).toHaveBeenCalledWith(CIRCLE_ID, USER_ID, true); - }); - - it("returns 401 when unauthenticated", async () => { - mockSession.mockResolvedValue(null); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(401); - }); - - it("returns 404 when circle not found", async () => { - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - mockGetCircle.mockResolvedValue(null); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(404); - }); - - it("returns 403 when joining private circle without token", async () => { - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - mockGetCircle.mockResolvedValue({ id: CIRCLE_ID, circleType: "private" } as any); - const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); - const json = await res.json(); - - expect(res.status).toBe(403); - expect(json.error).toContain("Invite token is required"); - }); - - it("returns 403 with invalid token for private circle", async () => { - const token = "invalid-token"; - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - mockGetCircle.mockResolvedValue({ id: CIRCLE_ID, circleType: "private" } as any); - mockVerifyToken.mockResolvedValue(null); - - const res = await POST(makeRequest({ token }), { params: { id: CIRCLE_ID } }); - expect(res.status).toBe(403); - }); -}); diff --git a/src/app/api/circles/[id]/join/route.ts b/src/app/api/circles/[id]/join/route.ts deleted file mode 100644 index d1ce546..0000000 --- a/src/app/api/circles/[id]/join/route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { joinCircleSchema } from "@/types/schemas"; -import { joinCircle, getCircleById } from "@/server/services/circle.service"; -import { withErrorHandler } from "@/server/middleware"; -import { verifyInviteToken } from "@/lib/tokens"; -import { checkReputationGate } from "@/server/services/reputation.service"; -import type { ApiResponse, Member } from "@/types"; - -export const POST = withErrorHandler(async (req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const body = await req.json(); - const parsed = joinCircleSchema.safeParse({ ...body, circleId: params.id }); - if (!parsed.success) { - return NextResponse.json>( - { success: false, error: parsed.error.errors[0].message }, - { status: 400 } - ); - } - - const { token } = parsed.data; - const circle = await getCircleById(params.id); - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - - let isInvited = false; - if (circle.circleType === "private") { - if (!token) { - return NextResponse.json>( - { success: false, error: "Invite token is required for private circles" }, - { status: 403 } - ); - } - const decoded = await verifyInviteToken(token); - if (!decoded || decoded.circleId !== params.id) { - return NextResponse.json>( - { success: false, error: "Invalid or expired invite token" }, - { status: 403 } - ); - } - isInvited = true; - } else if (token) { - // Also check token for public circles if provided - const decoded = await verifyInviteToken(token); - if (decoded && decoded.circleId === params.id) { - isInvited = true; - } - } - - // Check reputation gate if circle has minimum reputation requirement - if (circle.minReputation && circle.minReputation > 0) { - const { eligible, currentScore } = await checkReputationGate(userId, circle.minReputation); - if (!eligible) { - return NextResponse.json>( - { - success: false, - error: `This circle requires a minimum reputation score of ${circle.minReputation}. Your current score is ${currentScore}.`, - }, - { status: 403 } - ); - } - } - - const userId = (session.user as { id: string }).id; - const member = await joinCircle(params.id, userId, isInvited); - return NextResponse.json>({ success: true, data: member }, { status: 201 }); -}); diff --git a/src/app/api/circles/[id]/leave/route.ts b/src/app/api/circles/[id]/leave/route.ts deleted file mode 100644 index fa23bb8..0000000 --- a/src/app/api/circles/[id]/leave/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { leaveCircle } from "@/server/services/circle.service"; -import { withErrorHandler } from "@/server/middleware"; - -export const POST = withErrorHandler( - async (req: NextRequest, { params }: { params: { id: string } }) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); - } - - const circleId = params.id; - await leaveCircle(circleId, session.user.id); - - return NextResponse.json({ - success: true, - message: "Successfully left the circle", - }); - } -); diff --git a/src/app/api/circles/[id]/members/[memberId]/approve/route.ts b/src/app/api/circles/[id]/members/[memberId]/approve/route.ts deleted file mode 100644 index ac9fcd3..0000000 --- a/src/app/api/circles/[id]/members/[memberId]/approve/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { approveJoinRequest } from "@/server/services/circle.service"; -import { sendSms } from "@/lib/sms"; -import type { ApiResponse, Member } from "@/types"; - -export async function POST( - req: NextRequest, - { params }: { params: { id: string; memberId: string } } -): Promise>> { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { id: circleId, memberId } = params; - const member = await approveJoinRequest(circleId, memberId, session.user.id); - - // TODO: Send SMS notification to approved user - // await sendSms(member.userId, "Your join request has been approved!"); - - return NextResponse.json({ success: true, data: member }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to approve join request"; - return NextResponse.json( - { success: false, error: message }, - { status: 400 } - ); - } -} diff --git a/src/app/api/circles/[id]/members/[memberId]/reject/route.ts b/src/app/api/circles/[id]/members/[memberId]/reject/route.ts deleted file mode 100644 index 8ed768d..0000000 --- a/src/app/api/circles/[id]/members/[memberId]/reject/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { rejectJoinRequest } from "@/server/services/circle.service"; -import { sendSms } from "@/lib/sms"; -import type { ApiResponse, Member } from "@/types"; - -export async function POST( - req: NextRequest, - { params }: { params: { id: string; memberId: string } } -): Promise>> { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { id: circleId, memberId } = params; - const member = await rejectJoinRequest(circleId, memberId, session.user.id); - - // TODO: Send SMS notification to rejected user - // await sendSms(member.userId, "Your join request has been declined."); - - return NextResponse.json({ success: true, data: member }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to reject join request"; - return NextResponse.json( - { success: false, error: message }, - { status: 400 } - ); - } -} diff --git a/src/app/api/circles/[id]/pending-requests/route.ts b/src/app/api/circles/[id]/pending-requests/route.ts deleted file mode 100644 index 3f5b966..0000000 --- a/src/app/api/circles/[id]/pending-requests/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getPendingJoinRequests, getCircleById } from "@/server/services/circle.service"; -import type { ApiResponse, Member } from "@/types"; - -export async function GET( - req: NextRequest, - { params }: { params: { id: string } } -): Promise>> { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const circleId = params.id; - const circle = await getCircleById(circleId); - - if (!circle) { - return NextResponse.json( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - - // Only circle creator can view pending requests - if (circle.creatorId !== session.user.id) { - return NextResponse.json( - { success: false, error: "Only the circle creator can view pending requests" }, - { status: 403 } - ); - } - - const pendingRequests = await getPendingJoinRequests(circleId); - return NextResponse.json({ success: true, data: pendingRequests }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to fetch pending requests"; - return NextResponse.json( - { success: false, error: message }, - { status: 500 } - ); - } -} diff --git a/src/app/api/circles/[id]/route.ts b/src/app/api/circles/[id]/route.ts deleted file mode 100644 index 82bb3ca..0000000 --- a/src/app/api/circles/[id]/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getCircleById, getMembersByCircle } from "@/server/services/circle.service"; -import { getWaitlistStatus } from "@/server/services/waitlist.service"; -import { withErrorHandler } from "@/server/middleware"; -import type { ApiResponse, Circle, Member } from "@/types"; - -export const GET = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const { params } = ctx as { params: { id: string } }; - const circle = await getCircleById(params.id); - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - const circleMembers = await getMembersByCircle(params.id); - - const session = await getServerSession(authOptions); - const user = session?.user as { id: string } | undefined; - let waitlist = { isOnWaitlist: false, position: null as number | null }; - if (user?.id) { - waitlist = await getWaitlistStatus(params.id, user.id); - } - - return NextResponse.json>({ - success: true, - data: { circle, members: circleMembers, waitlist }, - }); -}); - -export const DELETE = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - const { params } = ctx as { params: { id: string } }; - const userId = (session.user as { id: string }).id; - const circle = await softDeleteCircle(params.id, userId); - return NextResponse.json>({ success: true, data: circle }); -}); diff --git a/src/app/api/circles/[id]/shuffle/route.ts b/src/app/api/circles/[id]/shuffle/route.ts deleted file mode 100644 index 375f096..0000000 --- a/src/app/api/circles/[id]/shuffle/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getCircleById, shuffleAndPersistPositions } from "@/server/services/circle.service"; -import { withErrorHandler } from "@/server/middleware"; -import type { ApiResponse, Member } from "@/types"; -import { randomBytes } from "crypto"; - -/** - * POST /api/circles/[id]/shuffle - * Randomizes payout positions for an open circle (creator only). - * Generates a seed, persists shuffled positions to DB, and stores seed for verifiability. - */ -export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const circle = await getCircleById(params.id); - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - - const userId = (session.user as { id: string }).id; - if (circle.creatorId !== userId) { - return NextResponse.json>( - { success: false, error: "Only the circle creator can shuffle positions" }, - { status: 403 } - ); - } - - if (circle.status !== "open") { - return NextResponse.json>( - { success: false, error: "Positions can only be shuffled before the circle starts" }, - { status: 400 } - ); - } - - // Generate deterministic seed from current timestamp and random bytes - const seed = `${Date.now()}-${randomBytes(16).toString("hex")}`; - - try { - const shuffled = await shuffleAndPersistPositions(params.id, seed); - return NextResponse.json>({ success: true, data: shuffled }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to shuffle positions"; - return NextResponse.json>( - { success: false, error: message }, - { status: 400 } - ); - } -}); diff --git a/src/app/api/circles/[id]/sync-payout-order/route.ts b/src/app/api/circles/[id]/sync-payout-order/route.ts deleted file mode 100644 index 9bce281..0000000 --- a/src/app/api/circles/[id]/sync-payout-order/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getCircleById, getMembersByCircle } from "@/server/services/circle.service"; -import { withErrorHandler } from "@/server/middleware"; -import { invokeContractSetPayoutOrder } from "@/lib/soroban"; -import type { ApiResponse } from "@/types"; - -/** - * POST /api/circles/[id]/sync-payout-order - * Syncs randomized payout order from DB to smart contract (admin/creator only). - * Must be called after shuffle and before circle starts. - */ -export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const circle = await getCircleById(params.id); - if (!circle) { - return NextResponse.json>( - { success: false, error: "Circle not found" }, - { status: 404 } - ); - } - - const userId = (session.user as { id: string }).id; - if (circle.creatorId !== userId) { - return NextResponse.json>( - { success: false, error: "Only the circle creator can sync payout order" }, - { status: 403 } - ); - } - - if (circle.status !== "open") { - return NextResponse.json>( - { success: false, error: "Payout order can only be synced before the circle starts" }, - { status: 400 } - ); - } - - if (circle.payoutMethod !== "randomized") { - return NextResponse.json>( - { success: false, error: "Circle is not using randomized payout method" }, - { status: 400 } - ); - } - - if (!circle.contractId) { - return NextResponse.json>( - { success: false, error: "Circle does not have a deployed smart contract" }, - { status: 400 } - ); - } - - try { - const members = await getMembersByCircle(params.id); - - // Build payout order: map member positions to their indices in the members array - // Members are sorted by position, so we need to find the original join order - const membersByJoinOrder = [...members].sort((a, b) => - new Date(a.joinedAt).getTime() - new Date(b.joinedAt).getTime() - ); - - const payoutOrder = members.map((m) => { - const joinIndex = membersByJoinOrder.findIndex((jm) => jm.id === m.id); - return joinIndex; - }); - - // Sync to smart contract - await invokeContractSetPayoutOrder(circle.contractId, payoutOrder); - - return NextResponse.json>( - { success: true, data: { payoutOrder } } - ); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to sync payout order"; - return NextResponse.json>( - { success: false, error: message }, - { status: 400 } - ); - } -}); diff --git a/src/app/api/circles/[id]/waitlist/route.ts b/src/app/api/circles/[id]/waitlist/route.ts deleted file mode 100644 index e22b1d4..0000000 --- a/src/app/api/circles/[id]/waitlist/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { withErrorHandler } from "@/server/middleware"; -import { - addToWaitlist, - removeFromWaitlist, - getWaitlistStatus, -} from "@/server/services/waitlist.service"; -import type { ApiResponse } from "@/types"; - -export const GET = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - const user = session?.user as { id: string } | undefined; - if (!user?.id) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const status = await getWaitlistStatus(params.id, user.id); - - return NextResponse.json({ - success: true, - data: status, - }); -}); - -export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - const user = session?.user as { id: string } | undefined; - if (!user?.id) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const status = await addToWaitlist(params.id, user.id); - - return NextResponse.json({ - success: true, - data: status, - }); -}); - -export const DELETE = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - const user = session?.user as { id: string } | undefined; - if (!user?.id) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const status = await removeFromWaitlist(params.id, user.id); - - return NextResponse.json({ - success: true, - data: status, - }); -}); diff --git a/src/app/api/circles/__tests__/route.test.ts b/src/app/api/circles/__tests__/route.test.ts deleted file mode 100644 index 008a861..0000000 --- a/src/app/api/circles/__tests__/route.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @jest-environment node - */ -import { GET, POST } from "@/app/api/circles/route"; -import { NextRequest } from "next/server"; -import { getServerSession } from "next-auth"; -import { createCircle, listOpenCircles, getCirclesByUser } from "@/server/services/circle.service"; - -jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); -jest.mock("@/lib/auth", () => ({ authOptions: {} })); -jest.mock("@/server/services/circle.service"); -jest.mock("@/server/middleware", () => ({ - withErrorHandler: (fn: Function) => fn, -})); - -const mockSession = getServerSession as jest.MockedFunction; -const mockCreateCircle = createCircle as jest.MockedFunction; -const mockListOpenCircles = listOpenCircles as jest.MockedFunction; -const mockGetCirclesByUser = getCirclesByUser as jest.MockedFunction; - -const USER_ID = "user-1"; - -describe("Circles API Routes", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("GET /api/circles", () => { - it("returns open circles with pagination", async () => { - const mockResult = { - data: [{ id: "1", name: "Circle 1" }], - total: 1, - page: 1, - limit: 10, - }; - mockListOpenCircles.mockResolvedValue(mockResult as any); - - const req = new NextRequest("http://localhost/api/circles?page=1&limit=10"); - const res = await GET(req); - const json = await res.json(); - - expect(res.status).toBe(200); - expect(json.success).toBe(true); - expect(json.data).toEqual(mockResult); - expect(mockListOpenCircles).toHaveBeenCalledWith(1, 10, expect.any(Object)); - }); - - it("returns user's circles when filter=mine is provided", async () => { - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - const mockCircles = [{ id: "1", name: "My Circle" }]; - mockGetCirclesByUser.mockResolvedValue(mockCircles as any); - - const req = new NextRequest("http://localhost/api/circles?filter=mine"); - const res = await GET(req); - const json = await res.json(); - - expect(res.status).toBe(200); - expect(json.success).toBe(true); - expect(json.data).toEqual(mockCircles); - expect(mockGetCirclesByUser).toHaveBeenCalledWith(USER_ID); - }); - - it("returns 401 for filter=mine when unauthenticated", async () => { - mockSession.mockResolvedValue(null); - - const req = new NextRequest("http://localhost/api/circles?filter=mine"); - const res = await GET(req); - - expect(res.status).toBe(401); - }); - }); - - describe("POST /api/circles", () => { - const validBody = { - name: "Test Circle", - contributionAmount: 5000, - contributionCurrency: "NGN", - maxMembers: 10, - cycleFrequency: "monthly", - payoutMethod: "fixed", - }; - - it("creates a new circle when authenticated and valid data", async () => { - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - const mockCircle = { id: "new-circle", ...validBody }; - mockCreateCircle.mockResolvedValue(mockCircle as any); - - const req = new NextRequest("http://localhost/api/circles", { - method: "POST", - body: JSON.stringify(validBody), - }); - const res = await POST(req); - const json = await res.json(); - - expect(res.status).toBe(201); - expect(json.success).toBe(true); - expect(json.data).toEqual(mockCircle); - expect(mockCreateCircle).toHaveBeenCalledWith(USER_ID, expect.objectContaining({ name: "Test Circle" })); - }); - - it("returns 401 when unauthenticated", async () => { - mockSession.mockResolvedValue(null); - - const req = new NextRequest("http://localhost/api/circles", { - method: "POST", - body: JSON.stringify(validBody), - }); - const res = await POST(req); - - expect(res.status).toBe(401); - }); - - it("returns 400 when validation fails", async () => { - mockSession.mockResolvedValue({ user: { id: USER_ID } } as any); - const invalidBody = { ...validBody, name: "" }; // Name too short - - const req = new NextRequest("http://localhost/api/circles", { - method: "POST", - body: JSON.stringify(invalidBody), - }); - const res = await POST(req); - const json = await res.json(); - - expect(res.status).toBe(400); - expect(json.success).toBe(false); - expect(json.error).toBeDefined(); - }); - }); -}); diff --git a/src/app/api/circles/route.ts b/src/app/api/circles/route.ts deleted file mode 100644 index ce18a9a..0000000 --- a/src/app/api/circles/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - const url = new URL(req.url); - const newUrl = url.pathname.replace('/api/circles', '/api/v1/circles'); - - return NextResponse.redirect(new URL(newUrl + url.search, url.origin), { - status: 301, - headers: { - 'X-API-Deprecated': 'true', - 'X-API-Deprecation-Info': 'This endpoint is deprecated. Use /api/v1/circles instead.', - } - }); -} - -export async function POST(req: NextRequest) { - const url = new URL(req.url); - const newUrl = url.pathname.replace('/api/circles', '/api/v1/circles'); - - return NextResponse.redirect(new URL(newUrl, url.origin), { - status: 308, // Preserve POST method - headers: { - 'X-API-Deprecated': 'true', - 'X-API-Deprecation-Info': 'This endpoint is deprecated. Use /api/v1/circles instead.', - } - }); -} diff --git a/src/app/api/cron/contribution-reminders/route.ts b/src/app/api/cron/contribution-reminders/route.ts deleted file mode 100644 index 5758272..0000000 --- a/src/app/api/cron/contribution-reminders/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { sendContributionReminders } from "@/server/services/scheduler.service"; -import { verifyCronSecret } from "@/lib/cron-auth"; - -/** - * Cron endpoint to send contribution reminders. - * - * Triggers the Contribution_Reminder_Service, which identifies active circle - * members who have not yet confirmed their contribution and whose cycle deadline - * (`next_payout_at`) falls within the 24-hour (23–25 h) or 2-hour (1–3 h) - * reminder windows. Eligible members receive an SMS via the SMS_Notification_System. - * - * Schedule: Run hourly so that both reminder windows are checked at least once - * per hour (mirrors the cadence of `/api/cron/reminders`). - * - * Authorization: Bearer - */ -export async function GET(req: NextRequest) { - const unauth = verifyCronSecret(req); - if (unauth) return unauth; - - try { - await sendContributionReminders(); - - return NextResponse.json({ - success: true, - message: "Contribution reminders sent successfully", - }); - } catch (error) { - console.error("Failed to send contribution reminders:", error); - return NextResponse.json( - { - success: false, - error: - error instanceof Error - ? error.message - : "Failed to send contribution reminders", - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/cron/cycle/__tests__/route.test.ts b/src/app/api/cron/cycle/__tests__/route.test.ts deleted file mode 100644 index e3d530a..0000000 --- a/src/app/api/cron/cycle/__tests__/route.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { GET } from "../route"; -import { NextRequest } from "next/server"; -import * as schedulerService from "@/server/services/scheduler.service"; -import { serverConfig } from "@/server/config"; - -jest.mock("@/server/services/scheduler.service"); - -const mockProcessDueCycles = schedulerService.processDueCycles as jest.MockedFunction< - typeof schedulerService.processDueCycles ->; - -// Cast to allow mutation in tests -const mutableConfig = serverConfig as { cronSecret: string }; - -function makeRequest(authHeader?: string): NextRequest { - const headers: Record = {}; - if (authHeader !== undefined) { - headers["authorization"] = authHeader; - } - return new NextRequest("http://localhost/api/cron/cycle", { headers }); -} - -describe("GET /api/cron/cycle", () => { - const VALID_SECRET = "test-cron-secret-xyz"; - const originalSecret = serverConfig.cronSecret; - - beforeEach(() => { - jest.clearAllMocks(); - mutableConfig.cronSecret = VALID_SECRET; - mockProcessDueCycles.mockResolvedValue(undefined); - }); - - afterEach(() => { - mutableConfig.cronSecret = originalSecret; - }); - - it("processes due cycles when authenticated with valid secret", async () => { - const req = makeRequest(`Bearer ${VALID_SECRET}`); - const res = await GET(req); - - expect(res.status).toBe(200); - const body = await res.json(); - expect(body).toEqual({ - success: true, - data: { message: "Cycle check complete" }, - }); - expect(mockProcessDueCycles).toHaveBeenCalledTimes(1); - }); - - it("returns 401 when Authorization header is missing", async () => { - const req = makeRequest(); - const res = await GET(req); - - expect(res.status).toBe(401); - const body = await res.json(); - expect(body).toEqual({ success: false, error: "Unauthorized" }); - expect(mockProcessDueCycles).not.toHaveBeenCalled(); - }); - - it("returns 401 when token is incorrect", async () => { - const req = makeRequest("Bearer wrong-token"); - const res = await GET(req); - - expect(res.status).toBe(401); - expect(mockProcessDueCycles).not.toHaveBeenCalled(); - }); - - it("returns 401 when CRON_SECRET is not configured", async () => { - mutableConfig.cronSecret = ""; - const req = makeRequest("Bearer anything"); - const res = await GET(req); - - expect(res.status).toBe(401); - expect(mockProcessDueCycles).not.toHaveBeenCalled(); - }); - - it("returns 401 when Authorization header has no Bearer prefix", async () => { - const req = makeRequest(VALID_SECRET); // missing "Bearer " - const res = await GET(req); - - expect(res.status).toBe(401); - expect(mockProcessDueCycles).not.toHaveBeenCalled(); - }); -}); diff --git a/src/app/api/cron/cycle/route.ts b/src/app/api/cron/cycle/route.ts deleted file mode 100644 index 4bada94..0000000 --- a/src/app/api/cron/cycle/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { processDueCycles } from "@/server/services/scheduler.service"; -import { verifyCronSecret } from "@/lib/cron-auth"; -import type { ApiResponse } from "@/types"; - -export const GET = async (req: NextRequest) => { - const unauth = verifyCronSecret(req); - if (unauth) return unauth; - - await processDueCycles(); - return NextResponse.json>({ - success: true, - data: { message: "Cycle check complete" }, - }); -}; diff --git a/src/app/api/cron/missed-contributions/route.ts b/src/app/api/cron/missed-contributions/route.ts deleted file mode 100644 index 10f91b3..0000000 --- a/src/app/api/cron/missed-contributions/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { processMissedContributions } from "@/server/services/scheduler.service"; -import { verifyCronSecret } from "@/lib/cron-auth"; - -/** - * Cron endpoint to process missed contributions - * Should be called daily by a cron service (Vercel Cron, GitHub Actions, etc.) - * - * Authorization: Bearer - */ -export async function GET(req: NextRequest) { - const unauth = verifyCronSecret(req); - if (unauth) return unauth; - - try { - await processMissedContributions(); - - return NextResponse.json({ - success: true, - message: "Missed contributions processed successfully", - }); - } catch (error) { - console.error("Failed to process missed contributions:", error); - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : "Failed to process missed contributions" - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/cron/reminders/route.ts b/src/app/api/cron/reminders/route.ts deleted file mode 100644 index 6b26693..0000000 --- a/src/app/api/cron/reminders/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { sendPayoutReminders } from "@/server/services/scheduler.service"; -import { verifyCronSecret } from "@/lib/cron-auth"; - -/** - * Cron endpoint to send payout reminders - * Should be called hourly by a cron service (Vercel Cron, GitHub Actions, etc.) - * - * Authorization: Bearer - */ -export async function GET(req: NextRequest) { - const unauth = verifyCronSecret(req); - if (unauth) return unauth; - - try { - await sendPayoutReminders(); - - return NextResponse.json({ - success: true, - message: "Payout reminders sent successfully", - }); - } catch (error) { - console.error("Failed to send payout reminders:", error); - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : "Failed to send reminders" - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/docs/route.ts b/src/app/api/docs/route.ts deleted file mode 100644 index 8d18a67..0000000 --- a/src/app/api/docs/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - const url = new URL(req.url); - const newUrl = url.pathname.replace('/api/docs', '/api/v1/docs'); - - return NextResponse.redirect(new URL(newUrl + url.search, url.origin), { - status: 301, - headers: { - 'X-API-Deprecated': 'true', - 'X-API-Deprecation-Info': 'This endpoint is deprecated. Use /api/v1/docs instead.', - } - }); -} diff --git a/src/app/api/docs/spec/route.ts b/src/app/api/docs/spec/route.ts deleted file mode 100644 index 078757a..0000000 --- a/src/app/api/docs/spec/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from "next/server"; -import { readFileSync } from "fs"; -import { join } from "path"; - -export function GET() { - const spec = readFileSync(join(process.cwd(), "docs/openapi.yaml"), "utf-8"); - return new NextResponse(spec, { - headers: { "Content-Type": "application/yaml; charset=utf-8" }, - }); -} diff --git a/src/app/api/fx/rate/route.ts b/src/app/api/fx/rate/route.ts deleted file mode 100644 index 12e7a16..0000000 --- a/src/app/api/fx/rate/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { getFiatPerUsdc } from "@/lib/fx"; -import type { ApiResponse } from "@/types"; - -export const dynamic = "force-dynamic"; - -export async function GET(req: NextRequest) { - const currency = req.nextUrl.searchParams.get("currency") || "NGN"; - - try { - const rate = await getFiatPerUsdc(currency); - return NextResponse.json>({ - success: true, - data: { rate, currency, fetchedAt: new Date().toISOString() }, - }); - } catch (err) { - return NextResponse.json>( - { success: false, error: `Failed to fetch FX rate for ${currency}` }, - { status: 500 } - ); - } -} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts deleted file mode 100644 index 6e922d1..0000000 --- a/src/app/api/health/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { withErrorHandler } from "@/server/middleware"; -import { getRedis } from "@/lib/redis"; -import { query, getPoolStats } from "@/lib/db"; - -export const GET = withErrorHandler(async (_req: NextRequest) => { - const start = Date.now(); - const health: any = { timestamp: new Date().toISOString() }; - - // DB check - try { - const dbStart = Date.now(); - await query("SELECT 1"); - const dbMs = Date.now() - dbStart; - health.db = dbMs < 500 ? "ok" : "degraded"; - health.dbMs = dbMs; - const stats = getPoolStats(); - if (stats) health.dbPool = stats; - } catch (err) { - health.db = "error"; - health.dbError = (err as Error).message; - } - - // Redis check - try { - const redisStart = Date.now(); - const redis = await getRedis(); - // ping returns "PONG" on success - // some redis clients use ping(), some use command; this should work with node-redis - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const pong = await redis.ping(); - const redisMs = Date.now() - redisStart; - health.redis = pong === "PONG" ? (redisMs < 500 ? "ok" : "degraded") : "error"; - health.redisMs = redisMs; - } catch (err) { - health.redis = "error"; - health.redisError = (err as Error).message; - } - - const totalMs = Date.now() - start; - health.status = health.db === "ok" && health.redis === "ok" ? "ok" : "degraded"; - health.totalMs = totalMs; - - return NextResponse.json(health); -}); -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - const url = new URL(req.url); - const newUrl = url.pathname.replace('/api/health', '/api/v1/health'); - - return NextResponse.redirect(new URL(newUrl + url.search, url.origin), { - status: 301, - headers: { - 'X-API-Deprecated': 'true', - 'X-API-Deprecation-Info': 'This endpoint is deprecated. Use /api/v1/health instead.', - } - }); -} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts deleted file mode 100644 index 70798e7..0000000 --- a/src/app/api/profile/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - const url = new URL(req.url); - const newUrl = url.pathname.replace('/api/profile', '/api/v1/profile'); - - return NextResponse.redirect(new URL(newUrl + url.search, url.origin), { - status: 301, - headers: { - 'X-API-Deprecated': 'true', - 'X-API-Deprecation-Info': 'This endpoint is deprecated. Use /api/v1/profile instead.', - } - }); -} - -export async function PUT(req: NextRequest) { - const url = new URL(req.url); - const newUrl = url.pathname.replace('/api/profile', '/api/v1/profile'); - - return NextResponse.redirect(new URL(newUrl, url.origin), { - status: 308, // Preserve PUT method - headers: { - 'X-API-Deprecated': 'true', - 'X-API-Deprecation-Info': 'This endpoint is deprecated. Use /api/v1/profile instead.', - } - }); -} diff --git a/src/app/api/reputation/route.ts b/src/app/api/reputation/route.ts deleted file mode 100644 index 982443c..0000000 --- a/src/app/api/reputation/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { calculateReputation, getReputation } from "@/server/services/reputation.service"; -import { withErrorHandler } from "@/server/middleware"; -import type { ApiResponse, ReputationScore } from "@/types"; - -// GET /api/reputation — fetch current user's score -export const GET = withErrorHandler(async (_req: NextRequest) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - const userId = (session.user as { id: string }).id; - const record = await getReputation(userId); - return NextResponse.json>({ - success: true, - data: record, - }); -}); - -// POST /api/reputation — recalculate score from contribution history -export const POST = withErrorHandler(async (req: NextRequest) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const body = await req.json(); - const { onTimeContributions = 0, circlesCompleted = 0, defaults = 0, stellarTxProof } = body; - - const userId = (session.user as { id: string }).id; - const record = await calculateReputation( - userId, - Number(onTimeContributions), - Number(circlesCompleted), - Number(defaults), - stellarTxProof - ); - - return NextResponse.json>({ success: true, data: record }); -}); diff --git a/src/app/api/user/sms-preferences/route.ts b/src/app/api/user/sms-preferences/route.ts deleted file mode 100644 index a61b915..0000000 --- a/src/app/api/user/sms-preferences/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { toggleSmsNotifications } from "@/server/services/notification.service"; -import { smsPreferencesSchema } from "@/types/schemas"; -import type { ApiResponse } from "@/types"; - -export async function POST(req: NextRequest): Promise>> { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const body = await req.json(); - const parsed = smsPreferencesSchema.safeParse(body); - if (!parsed.success) { - return NextResponse.json( - { success: false, error: parsed.error.errors[0].message }, - { status: 400 } - ); - } - - const { enabled } = parsed.data; - await toggleSmsNotifications(session.user.id, enabled); - - return NextResponse.json({ - success: true, - data: { enabled } - }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to update SMS preferences"; - return NextResponse.json( - { success: false, error: message }, - { status: 500 } - ); - } -} diff --git a/src/app/api/users/[id]/reputation/route.ts b/src/app/api/users/[id]/reputation/route.ts deleted file mode 100644 index f218ef3..0000000 --- a/src/app/api/users/[id]/reputation/route.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { getReputationStats, verifyReputation } from "@/lib/reputation"; -import { query } from "@/lib/db"; -import { withErrorHandler } from "@/server/middleware"; -import type { ApiResponse } from "@/types"; - -/** - * GET /api/users/[id]/reputation - * Fetch on-chain reputation for a user - */ -export const GET = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json>( - { success: false, error: "Unauthorized" }, - { status: 401 } - ); - } - - const { params } = ctx as { params: { id: string } }; - const { searchParams } = new URL(_req.url); - const verify = searchParams.get("verify") === "true"; - - // Fetch user's Stellar address - const { rows } = await query<{ stellar_public_key: string | null }>( - "SELECT stellar_public_key FROM users WHERE id = $1", - [params.id] - ); - - if (rows.length === 0) { - return NextResponse.json>( - { success: false, error: "User not found" }, - { status: 404 } - ); - } - - const stellarAddress = rows[0].stellar_public_key; - if (!stellarAddress) { - return NextResponse.json>( - { success: false, error: "User has no Stellar address" }, - { status: 400 } - ); - } - - // Fetch on-chain reputation stats - const stats = await getReputationStats(stellarAddress); - - // Optionally verify against database - let isVerified: boolean | undefined; - if (verify) { - isVerified = await verifyReputation(params.id, stellarAddress); - } - - return NextResponse.json< - ApiResponse<{ - score: number; - circlesCompleted: number; - onTimeContributions: number; - totalContributions: number; - isVerified?: boolean; - }> - >({ - success: true, - data: { - ...stats, - ...(verify && { isVerified }), - }, - }); -}); diff --git a/src/app/api/admin/circles/route.ts b/src/app/api/v1/admin/circles/deleted/route.ts similarity index 50% rename from src/app/api/admin/circles/route.ts rename to src/app/api/v1/admin/circles/deleted/route.ts index 037d5f7..ce73e34 100644 --- a/src/app/api/admin/circles/route.ts +++ b/src/app/api/v1/admin/circles/deleted/route.ts @@ -1,13 +1,12 @@ -import { NextRequest, NextResponse } from "next/server"; -import { adminListCircles } from "@/server/services/admin.service"; +import { NextResponse } from "next/server"; +import { adminListDeletedCircles } from "@/server/services/admin.service"; import { withAdminAuth, withErrorHandler } from "@/server/middleware"; import type { ApiResponse } from "@/types"; import type { AdminCircleRow } from "@/server/services/admin.service"; export const GET = withErrorHandler( - withAdminAuth(async (req: NextRequest) => { - const includeDeleted = new URL(req.url).searchParams.get("includeDeleted") === "true"; - const circles = await adminListCircles(includeDeleted); + withAdminAuth(async () => { + const circles = await adminListDeletedCircles(); return NextResponse.json>({ success: true, data: circles }); }) ); diff --git a/src/app/api/v1/admin/users/[id]/route.ts b/src/app/api/v1/admin/users/[id]/route.ts new file mode 100644 index 0000000..1287391 --- /dev/null +++ b/src/app/api/v1/admin/users/[id]/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from "next/server"; +import { adminSoftDeleteUser } from "@/server/services/admin.service"; +import { withAdminAuth, withErrorHandler } from "@/server/middleware"; +import type { ApiResponse } from "@/types"; + +export const DELETE = withErrorHandler( + withAdminAuth(async (_req: NextRequest, ctx: unknown) => { + const { params } = ctx as { params: { id: string } }; + await adminSoftDeleteUser(params.id); + return NextResponse.json>({ success: true, data: null }); + }) +); diff --git a/src/app/api/analytics/route.ts b/src/app/api/v1/analytics/route.ts similarity index 100% rename from src/app/api/analytics/route.ts rename to src/app/api/v1/analytics/route.ts diff --git a/src/app/api/auth/refresh/__tests__/route.test.ts b/src/app/api/v1/auth/refresh/__tests__/route.test.ts similarity index 95% rename from src/app/api/auth/refresh/__tests__/route.test.ts rename to src/app/api/v1/auth/refresh/__tests__/route.test.ts index e4ee4b9..e3d3372 100644 --- a/src/app/api/auth/refresh/__tests__/route.test.ts +++ b/src/app/api/v1/auth/refresh/__tests__/route.test.ts @@ -10,7 +10,10 @@ jest.mock("@/lib/db"); jest.mock("@/server/middleware", () => ({ withErrorHandler: (handler: Function) => handler, })); -jest.mock("jose"); +jest.mock("jose", () => ({ + ...jest.requireActual("jose"), + SignJWT: jest.fn(), +})); describe("POST /api/auth/refresh", () => { beforeEach(() => { @@ -112,12 +115,12 @@ describe("POST /api/auth/refresh", () => { const setCookieHeader = response.headers.get("set-cookie"); expect(setCookieHeader).toContain("refreshToken=new_refresh_token"); expect(setCookieHeader).toContain("HttpOnly"); - expect(setCookieHeader).toContain("SameSite=Lax"); + expect(setCookieHeader?.toLowerCase()).toContain("samesite=lax"); }); it("should set secure cookie in production", async () => { const originalEnv = process.env.NODE_ENV; - process.env.NODE_ENV = "production"; + (process.env as any).NODE_ENV = "production"; const req = new NextRequest("http://localhost:3000/api/auth/refresh", { method: "POST", @@ -152,6 +155,6 @@ describe("POST /api/auth/refresh", () => { const setCookieHeader = response.headers.get("set-cookie"); expect(setCookieHeader).toContain("Secure"); - process.env.NODE_ENV = originalEnv; + (process.env as any).NODE_ENV = originalEnv; }); }); diff --git a/src/app/api/v1/auth/send-otp/route.ts b/src/app/api/v1/auth/send-otp/route.ts index 15b7910..a1e35d1 100644 --- a/src/app/api/v1/auth/send-otp/route.ts +++ b/src/app/api/v1/auth/send-otp/route.ts @@ -77,8 +77,8 @@ export const POST = withErrorHandler(async (req: NextRequest) => { data: { message: "Account is locked", lockout: lockoutStatus, - } as any, - }, + }, + } as any, { status: 423 } ); } diff --git a/src/app/api/auth/verify-otp/__tests__/route.test.ts b/src/app/api/v1/auth/verify-otp/__tests__/route.test.ts similarity index 100% rename from src/app/api/auth/verify-otp/__tests__/route.test.ts rename to src/app/api/v1/auth/verify-otp/__tests__/route.test.ts diff --git a/src/app/api/v1/auth/verify-otp/route.ts b/src/app/api/v1/auth/verify-otp/route.ts index dd3a489..5cf407f 100644 --- a/src/app/api/v1/auth/verify-otp/route.ts +++ b/src/app/api/v1/auth/verify-otp/route.ts @@ -102,8 +102,8 @@ export const POST = withErrorHandler(async (req: NextRequest) => { data: { message: "Account is locked", lockout: lockoutStatus, - } as any, - }, + }, + } as any, { status: 423 } ); } @@ -129,8 +129,8 @@ export const POST = withErrorHandler(async (req: NextRequest) => { data: { message: "OTP verification failed", lockout: updatedStatus, - } as any, - }, + }, + } as any, { status: statusCode } ); } diff --git a/src/app/api/circles/[id]/chat/route.ts b/src/app/api/v1/circles/[id]/chat/route.ts similarity index 94% rename from src/app/api/circles/[id]/chat/route.ts rename to src/app/api/v1/circles/[id]/chat/route.ts index 2c143f3..b5d6644 100644 --- a/src/app/api/circles/[id]/chat/route.ts +++ b/src/app/api/v1/circles/[id]/chat/route.ts @@ -7,7 +7,7 @@ import { getMessages, postMessage } from "@/server/services/chat.service"; import { broadcastChatMessage } from "@/server/websocket"; import type { ApiResponse, CircleMessage } from "@/types"; -// ─── GET /api/circles/[id]/chat ─────────────────────────────────────────────── +// ─── GET /api/v1/circles/[id]/chat ─────────────────────────────────────────────── export const GET = withErrorHandler(async (req: NextRequest, ctx: unknown) => { const session = await getServerSession(authOptions); @@ -71,7 +71,7 @@ export const GET = withErrorHandler(async (req: NextRequest, ctx: unknown) => { ); }); -// ─── POST /api/circles/[id]/chat ────────────────────────────────────────────── +// ─── POST /api/v1/circles/[id]/chat ────────────────────────────────────────────── export const POST = withErrorHandler( withRateLimit( diff --git a/src/app/api/v1/circles/[id]/contribute/__tests__/route.test.ts b/src/app/api/v1/circles/[id]/contribute/__tests__/route.test.ts index 509e8ec..b763184 100644 --- a/src/app/api/v1/circles/[id]/contribute/__tests__/route.test.ts +++ b/src/app/api/v1/circles/[id]/contribute/__tests__/route.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment node */ -import { POST } from "@/app/api/circles/[id]/contribute/route"; +import { POST } from "@/app/api/v1/circles/[id]/contribute/route"; import { NextRequest } from "next/server"; jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); @@ -13,7 +13,8 @@ jest.mock("@/server/config", () => ({ serverConfig: { app: { url: "http://localhost:3000" }, paystack: { secretKey: "test" }, - stellar: { network: "testnet", sorobanRpcUrl: "http://localhost", ajoContractId: "test" }, + stellar: { network: "testnet", sorobanRpcUrl: "http://localhost", horizonUrl: "https://horizon-testnet.stellar.org", ajoContractId: "test" }, + usdc: { assetCode: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, }, })); jest.mock("@/server/middleware", () => ({ @@ -106,7 +107,7 @@ describe("POST /api/circles/[id]/contribute", () => { mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // Upsert insert mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); - mockInitPayment.mockResolvedValue({ authorizationUrl: authUrl, reference: `ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}` }); + mockInitPayment.mockResolvedValue({ authorizationUrl: authUrl, reference: `ajo-${CIRCLE_ID}-${MEMBER_ID}-${CYCLE}`, platformFee: 0 }); const res = await POST(makeRequest(), { params: { id: CIRCLE_ID } }); const json = await res.json(); diff --git a/src/app/api/v1/circles/[id]/contribute/route.ts b/src/app/api/v1/circles/[id]/contribute/route.ts index d5248a0..d73d9ab 100644 --- a/src/app/api/v1/circles/[id]/contribute/route.ts +++ b/src/app/api/v1/circles/[id]/contribute/route.ts @@ -63,7 +63,7 @@ export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => const callbackUrl = `${serverConfig.app.url}/circles/${params.id}/contribute/callback?reference=${reference}`; - const { authorizationUrl } = await initializePayment({ + const { authorizationUrl, platformFee } = await initializePayment({ email: (session.user as { email?: string }).email ?? `${userId}@ajosave.app`, amount: circle.contributionFiat, currency: circle.contributionCurrency, @@ -86,8 +86,8 @@ export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => [randomUUID(), params.id, member.id, circle.currentCycle, circle.contributionUsdc, reference, authorizationUrl] ); - return NextResponse.json>({ + return NextResponse.json>({ success: true, - data: { authorizationUrl, reference }, + data: { authorizationUrl, reference, platformFee }, }); }); diff --git a/src/app/api/v1/circles/[id]/contribute/verify/__tests__/route.test.ts b/src/app/api/v1/circles/[id]/contribute/verify/__tests__/route.test.ts index a653429..d51c464 100644 --- a/src/app/api/v1/circles/[id]/contribute/verify/__tests__/route.test.ts +++ b/src/app/api/v1/circles/[id]/contribute/verify/__tests__/route.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment node */ -import { GET } from "@/app/api/circles/[id]/contribute/verify/route"; +import { GET } from "@/app/api/v1/circles/[id]/contribute/verify/route"; import { NextRequest } from "next/server"; jest.mock("next-auth", () => ({ getServerSession: jest.fn() })); @@ -13,7 +13,11 @@ jest.mock("@/server/middleware", () => ({ withErrorHandler: (fn: Function) => fn, })); jest.mock("@/server/config", () => ({ - serverConfig: { paystack: { secretKey: "test" }, stellar: { network: "testnet", sorobanRpcUrl: "http://localhost", ajoContractId: "test" } }, + serverConfig: { + paystack: { secretKey: "test" }, + stellar: { network: "testnet", sorobanRpcUrl: "http://localhost", horizonUrl: "https://horizon-testnet.stellar.org", ajoContractId: "test" }, + usdc: { assetCode: "USDC", issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" }, + }, })); import { getServerSession } from "next-auth"; diff --git a/src/app/api/v1/circles/[id]/contribute/verify/route.ts b/src/app/api/v1/circles/[id]/contribute/verify/route.ts index 36dca20..f904ce7 100644 --- a/src/app/api/v1/circles/[id]/contribute/verify/route.ts +++ b/src/app/api/v1/circles/[id]/contribute/verify/route.ts @@ -21,15 +21,16 @@ export const GET = withErrorHandler(async (req: NextRequest) => { // Send SMS confirmation if payment was successful if (result.status === "success") { const session = await getServerSession(authOptions); + const user = session?.user as { id: string } | undefined; const circleId = req.nextUrl.searchParams.get("circleId"); const cycleNumber = req.nextUrl.searchParams.get("cycleNumber"); - if (session?.user?.id && circleId && cycleNumber) { + if (user?.id && circleId && cycleNumber) { const circle = await getCircleById(circleId); if (circle) { // Send notification (async, don't block) notifyContributionReceived( - session.user.id, + user.id, circle.name, circle.contributionUsdc, parseInt(cycleNumber) diff --git a/src/app/api/circles/[id]/contributions/route.ts b/src/app/api/v1/circles/[id]/contributions/route.ts similarity index 100% rename from src/app/api/circles/[id]/contributions/route.ts rename to src/app/api/v1/circles/[id]/contributions/route.ts diff --git a/src/app/api/circles/[id]/disputes/route.ts b/src/app/api/v1/circles/[id]/disputes/route.ts similarity index 95% rename from src/app/api/circles/[id]/disputes/route.ts rename to src/app/api/v1/circles/[id]/disputes/route.ts index b857326..0483923 100644 --- a/src/app/api/circles/[id]/disputes/route.ts +++ b/src/app/api/v1/circles/[id]/disputes/route.ts @@ -32,7 +32,8 @@ export const GET = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { export const POST = withErrorHandler(async (req: NextRequest, ctx: unknown) => { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json>( { success: false, error: "Unauthorized" }, { status: 401 } diff --git a/src/app/api/v1/circles/[id]/join/__tests__/route.test.ts b/src/app/api/v1/circles/[id]/join/__tests__/route.test.ts index c121b77..74d6b5c 100644 --- a/src/app/api/v1/circles/[id]/join/__tests__/route.test.ts +++ b/src/app/api/v1/circles/[id]/join/__tests__/route.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment node */ -import { POST } from "@/app/api/circles/[id]/join/route"; +import { POST } from "@/app/api/v1/circles/[id]/join/route"; import { NextRequest } from "next/server"; import { getServerSession } from "next-auth"; import { getCircleById, joinCircle } from "@/server/services/circle.service"; @@ -21,7 +21,7 @@ const mockJoinCircle = joinCircle as jest.MockedFunction; const mockVerifyToken = verifyInviteToken as jest.MockedFunction; const USER_ID = "user-1"; -const CIRCLE_ID = "circle-1"; +const CIRCLE_ID = "123e4567-e89b-12d3-a456-426614174000"; describe("POST /api/circles/[id]/join", () => { beforeEach(() => { diff --git a/src/app/api/v1/circles/[id]/join/route.ts b/src/app/api/v1/circles/[id]/join/route.ts index d1ce546..e23ba5a 100644 --- a/src/app/api/v1/circles/[id]/join/route.ts +++ b/src/app/api/v1/circles/[id]/join/route.ts @@ -17,6 +17,7 @@ export const POST = withErrorHandler(async (req: NextRequest, ctx: unknown) => { ); } + const userId = (session.user as { id: string }).id; const { params } = ctx as { params: { id: string } }; const body = await req.json(); const parsed = joinCircleSchema.safeParse({ ...body, circleId: params.id }); @@ -74,7 +75,6 @@ export const POST = withErrorHandler(async (req: NextRequest, ctx: unknown) => { } } - const userId = (session.user as { id: string }).id; const member = await joinCircle(params.id, userId, isInvited); return NextResponse.json>({ success: true, data: member }, { status: 201 }); }); diff --git a/src/app/api/v1/circles/[id]/leave/route.ts b/src/app/api/v1/circles/[id]/leave/route.ts index fa23bb8..966fb61 100644 --- a/src/app/api/v1/circles/[id]/leave/route.ts +++ b/src/app/api/v1/circles/[id]/leave/route.ts @@ -5,14 +5,16 @@ import { leaveCircle } from "@/server/services/circle.service"; import { withErrorHandler } from "@/server/middleware"; export const POST = withErrorHandler( - async (req: NextRequest, { params }: { params: { id: string } }) => { + async (req: NextRequest, ctx: unknown) => { const session = await getServerSession(authOptions); - if (!session?.user) { + const user = session?.user as { id: string } | undefined; + if (!user) { return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); } + const { params } = ctx as { params: { id: string } }; const circleId = params.id; - await leaveCircle(circleId, session.user.id); + await leaveCircle(circleId, user.id); return NextResponse.json({ success: true, diff --git a/src/app/api/v1/circles/[id]/members/[memberId]/approve/route.ts b/src/app/api/v1/circles/[id]/members/[memberId]/approve/route.ts index ac9fcd3..b6c9edd 100644 --- a/src/app/api/v1/circles/[id]/members/[memberId]/approve/route.ts +++ b/src/app/api/v1/circles/[id]/members/[memberId]/approve/route.ts @@ -11,7 +11,8 @@ export async function POST( ): Promise>> { try { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json( { success: false, error: "Unauthorized" }, { status: 401 } @@ -19,7 +20,7 @@ export async function POST( } const { id: circleId, memberId } = params; - const member = await approveJoinRequest(circleId, memberId, session.user.id); + const member = await approveJoinRequest(circleId, memberId, user.id); // TODO: Send SMS notification to approved user // await sendSms(member.userId, "Your join request has been approved!"); diff --git a/src/app/api/v1/circles/[id]/members/[memberId]/reject/route.ts b/src/app/api/v1/circles/[id]/members/[memberId]/reject/route.ts index 8ed768d..8026dc4 100644 --- a/src/app/api/v1/circles/[id]/members/[memberId]/reject/route.ts +++ b/src/app/api/v1/circles/[id]/members/[memberId]/reject/route.ts @@ -11,7 +11,8 @@ export async function POST( ): Promise>> { try { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json( { success: false, error: "Unauthorized" }, { status: 401 } @@ -19,7 +20,7 @@ export async function POST( } const { id: circleId, memberId } = params; - const member = await rejectJoinRequest(circleId, memberId, session.user.id); + const member = await rejectJoinRequest(circleId, memberId, user.id); // TODO: Send SMS notification to rejected user // await sendSms(member.userId, "Your join request has been declined."); diff --git a/src/app/api/v1/circles/[id]/pause/route.ts b/src/app/api/v1/circles/[id]/pause/route.ts new file mode 100644 index 0000000..4204a08 --- /dev/null +++ b/src/app/api/v1/circles/[id]/pause/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { pauseCircle } from "@/server/services/circle.service"; +import { withErrorHandler } from "@/server/middleware"; +import type { ApiResponse, Circle } from "@/types"; + +export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json>( + { success: false, error: "Unauthorized" }, + { status: 401 } + ); + } + + const { params } = ctx as { params: { id: string } }; + const userId = (session.user as { id: string }).id; + + try { + const circle = await pauseCircle(params.id, userId); + return NextResponse.json>({ success: true, data: circle }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to pause circle"; + const status = + message === "Circle not found" ? 404 : + message === "Only creator can pause the circle" ? 403 : 400; + return NextResponse.json>( + { success: false, error: message }, + { status } + ); + } +}); diff --git a/src/app/api/circles/[id]/payouts/route.ts b/src/app/api/v1/circles/[id]/payouts/route.ts similarity index 81% rename from src/app/api/circles/[id]/payouts/route.ts rename to src/app/api/v1/circles/[id]/payouts/route.ts index 0f078a9..d1eff42 100644 --- a/src/app/api/circles/[id]/payouts/route.ts +++ b/src/app/api/v1/circles/[id]/payouts/route.ts @@ -1,16 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { query } from "@/lib/db"; import { withErrorHandler } from "@/server/middleware"; -import type { ApiResponse } from "@/types"; +import type { ApiResponse, PayoutHistoryRow } from "@/types"; -export interface PayoutHistoryRow { - id: string; - cycleNumber: number; - amountUsdc: string; - txHash: string; - paidAt: string; - recipientName: string; -} +export type { PayoutHistoryRow }; export const GET = withErrorHandler(async ( _req: NextRequest, diff --git a/src/app/api/v1/circles/[id]/pending-requests/route.ts b/src/app/api/v1/circles/[id]/pending-requests/route.ts index 3f5b966..c4664ad 100644 --- a/src/app/api/v1/circles/[id]/pending-requests/route.ts +++ b/src/app/api/v1/circles/[id]/pending-requests/route.ts @@ -10,7 +10,8 @@ export async function GET( ): Promise>> { try { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json( { success: false, error: "Unauthorized" }, { status: 401 } @@ -28,7 +29,7 @@ export async function GET( } // Only circle creator can view pending requests - if (circle.creatorId !== session.user.id) { + if (circle.creatorId !== user.id) { return NextResponse.json( { success: false, error: "Only the circle creator can view pending requests" }, { status: 403 } diff --git a/src/app/api/circles/[id]/cancel/route.ts b/src/app/api/v1/circles/[id]/resume/route.ts similarity index 82% rename from src/app/api/circles/[id]/cancel/route.ts rename to src/app/api/v1/circles/[id]/resume/route.ts index d7ad3c1..20cbf51 100644 --- a/src/app/api/circles/[id]/cancel/route.ts +++ b/src/app/api/v1/circles/[id]/resume/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; -import { cancelCircle } from "@/server/services/circle.service"; +import { resumeCircle } from "@/server/services/circle.service"; import { withErrorHandler } from "@/server/middleware"; import type { ApiResponse, Circle } from "@/types"; @@ -18,13 +18,13 @@ export const POST = withErrorHandler(async (_req: NextRequest, ctx: unknown) => const userId = (session.user as { id: string }).id; try { - const circle = await cancelCircle(params.id, userId); + const circle = await resumeCircle(params.id, userId); return NextResponse.json>({ success: true, data: circle }); } catch (error) { - const message = error instanceof Error ? error.message : "Failed to cancel circle"; + const message = error instanceof Error ? error.message : "Failed to resume circle"; const status = message === "Circle not found" ? 404 : - message === "Only the creator can cancel a circle" ? 403 : 400; + message === "Only creator can resume the circle" ? 403 : 400; return NextResponse.json>( { success: false, error: message }, { status } diff --git a/src/app/api/v1/circles/[id]/route.ts b/src/app/api/v1/circles/[id]/route.ts index fffc25f..897593f 100644 --- a/src/app/api/v1/circles/[id]/route.ts +++ b/src/app/api/v1/circles/[id]/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; -import { getCircleById, getMembersByCircle } from "@/server/services/circle.service"; +import { getCircleById, getMembersByCircle, deleteCircle } from "@/server/services/circle.service"; import { getWaitlistStatus } from "@/server/services/waitlist.service"; import { withErrorHandler } from "@/server/middleware"; import type { ApiResponse, Circle, Member } from "@/types"; @@ -29,3 +29,17 @@ export const GET = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { data: { circle, members: circleMembers, waitlist }, }); }); + +export const DELETE = withErrorHandler(async (_req: NextRequest, ctx: unknown) => { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json>( + { success: false, error: "Unauthorized" }, + { status: 401 } + ); + } + const { params } = ctx as { params: { id: string } }; + const userId = (session.user as { id: string }).id; + await deleteCircle(params.id, userId); + return NextResponse.json>({ success: true, data: { deleted: true } }); +}); diff --git a/src/app/api/v1/circles/__tests__/route.test.ts b/src/app/api/v1/circles/__tests__/route.test.ts index 008a861..e038a29 100644 --- a/src/app/api/v1/circles/__tests__/route.test.ts +++ b/src/app/api/v1/circles/__tests__/route.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment node */ -import { GET, POST } from "@/app/api/circles/route"; +import { GET, POST } from "@/app/api/v1/circles/route"; import { NextRequest } from "next/server"; import { getServerSession } from "next-auth"; import { createCircle, listOpenCircles, getCirclesByUser } from "@/server/services/circle.service"; @@ -11,6 +11,7 @@ jest.mock("@/lib/auth", () => ({ authOptions: {} })); jest.mock("@/server/services/circle.service"); jest.mock("@/server/middleware", () => ({ withErrorHandler: (fn: Function) => fn, + withRateLimit: (fn: Function) => fn, })); const mockSession = getServerSession as jest.MockedFunction; diff --git a/src/app/api/v1/cron/cycle/__tests__/route.test.ts b/src/app/api/v1/cron/cycle/__tests__/route.test.ts index e3d530a..80c0da1 100644 --- a/src/app/api/v1/cron/cycle/__tests__/route.test.ts +++ b/src/app/api/v1/cron/cycle/__tests__/route.test.ts @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ import { GET } from "../route"; import { NextRequest } from "next/server"; import * as schedulerService from "@/server/services/scheduler.service"; diff --git a/src/app/api/og/circle/route.ts b/src/app/api/v1/og/circle/route.ts similarity index 100% rename from src/app/api/og/circle/route.ts rename to src/app/api/v1/og/circle/route.ts diff --git a/src/app/api/v1/profile/route.ts b/src/app/api/v1/profile/route.ts index c08a55b..3455a5b 100644 --- a/src/app/api/v1/profile/route.ts +++ b/src/app/api/v1/profile/route.ts @@ -31,7 +31,8 @@ export type ProfileData = { export async function GET(): Promise>> { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); } @@ -56,7 +57,7 @@ export async function GET(): Promise>> { LEFT JOIN contributions c ON c.member_id = m.id WHERE u.id = $1 GROUP BY u.id`, - [session.user.id] + [user.id] ); if (!rows[0]) { @@ -86,7 +87,8 @@ export async function PATCH( req: NextRequest ): Promise>> { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); } @@ -111,7 +113,7 @@ export async function PATCH( displayName ?? null, email !== undefined ? (email === "" ? null : email) : null, stellarPublicKey !== undefined ? (stellarPublicKey === "" ? null : stellarPublicKey) : null, - session.user.id, + user.id, ] ); diff --git a/src/app/api/referral/route.ts b/src/app/api/v1/referral/route.ts similarity index 95% rename from src/app/api/referral/route.ts rename to src/app/api/v1/referral/route.ts index 0a40d6c..accf45e 100644 --- a/src/app/api/referral/route.ts +++ b/src/app/api/v1/referral/route.ts @@ -16,7 +16,7 @@ function generateCode(): string { return randomBytes(4).toString("hex").toUpperCase(); // e.g. "A3F2B1C9" } -/** GET /api/referral — return the current user's referral code and stats */ +/** GET /api/v1/referral — return the current user's referral code and stats */ export const GET = withErrorHandler(async () => { const session = await getServerSession(authOptions); if (!session?.user) { @@ -63,7 +63,7 @@ export const GET = withErrorHandler(async () => { }); }); -/** POST /api/referral — apply a referral code (one-time, before first contribution) */ +/** POST /api/v1/referral — apply a referral code (one-time, before first contribution) */ export const POST = withErrorHandler(async (req: NextRequest) => { const session = await getServerSession(authOptions); if (!session?.user) { diff --git a/src/app/api/v1/reputation/route.ts b/src/app/api/v1/reputation/route.ts new file mode 100644 index 0000000..d103206 --- /dev/null +++ b/src/app/api/v1/reputation/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { getUserReputation, getReputationLevel } from "@/server/services/reputation.service"; +import { withErrorHandler } from "@/server/middleware"; +import type { ApiResponse, ReputationScore } from "@/types"; + +// GET /api/v1/reputation — fetch current user's score +export const GET = withErrorHandler(async (_req: NextRequest) => { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json>( + { success: false, error: "Unauthorized" }, + { status: 401 } + ); + } + const userId = (session.user as { id: string }).id; + const score = await getUserReputation(userId); + const level = getReputationLevel(score); + + return NextResponse.json>({ + success: true, + data: { + score, + level, + }, + }); +}); + +// POST /api/v1/reputation — sync reputation score with on-chain Soroban contract +export const POST = withErrorHandler(async (_req: NextRequest) => { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json>( + { success: false, error: "Unauthorized" }, + { status: 401 } + ); + } + + const userId = (session.user as { id: string }).id; + + // Retrieve user's stellar public key from DB + const { query } = await import("@/lib/db"); + const { rows } = await query<{ stellar_public_key: string | null }>( + "SELECT stellar_public_key FROM users WHERE id = $1", + [userId] + ); + + const stellarAddress = rows[0]?.stellar_public_key; + if (stellarAddress) { + const { syncReputationToDb } = await import("@/lib/reputation"); + await syncReputationToDb(userId, stellarAddress); + } + + const score = await getUserReputation(userId); + const level = getReputationLevel(score); + + return NextResponse.json>({ + success: true, + data: { + score, + level, + }, + }); +}); diff --git a/src/app/api/stellar/balance/route.ts b/src/app/api/v1/stellar/balance/route.ts similarity index 100% rename from src/app/api/stellar/balance/route.ts rename to src/app/api/v1/stellar/balance/route.ts diff --git a/src/app/api/v1/user/sms-preferences/route.ts b/src/app/api/v1/user/sms-preferences/route.ts index a61b915..4fc15d8 100644 --- a/src/app/api/v1/user/sms-preferences/route.ts +++ b/src/app/api/v1/user/sms-preferences/route.ts @@ -8,7 +8,8 @@ import type { ApiResponse } from "@/types"; export async function POST(req: NextRequest): Promise>> { try { const session = await getServerSession(authOptions); - if (!session?.user?.id) { + const user = session?.user as { id: string } | undefined; + if (!user?.id) { return NextResponse.json( { success: false, error: "Unauthorized" }, { status: 401 } @@ -25,7 +26,7 @@ export async function POST(req: NextRequest): Promise ({ + query: jest.fn(), + transaction: jest.fn((cb) => cb(jest.fn())), +})); jest.mock("@/server/config", () => ({ serverConfig: { paystack: { secretKey: "test-secret" } }, })); +jest.mock("@/lib/logger", () => ({ + warn: jest.fn(), + info: jest.fn(), + error: jest.fn(), +})); import * as db from "@/lib/db"; const mockQuery = db.query as jest.MockedFunction; +const mockTransaction = db.transaction as jest.MockedFunction; const SECRET = "test-secret"; @@ -20,7 +29,7 @@ function makeRequest(body: object, signature?: string): NextRequest { const sig = signature ?? createHmac("sha512", SECRET).update(raw).digest("hex"); - return new NextRequest("http://localhost/api/webhooks/paystack", { + return new NextRequest("http://localhost/api/v1/webhooks/paystack", { method: "POST", headers: { "x-paystack-signature": sig, "content-type": "application/json" }, body: raw, @@ -28,13 +37,14 @@ function makeRequest(body: object, signature?: string): NextRequest { } const CHARGE_SUCCESS = { + id: "evt_123", event: "charge.success", data: { reference: "ajo-circle-1-member-1-2" }, }; beforeEach(() => jest.clearAllMocks()); -describe("POST /api/webhooks/paystack", () => { +describe("POST /api/v1/webhooks/paystack", () => { it("returns 401 for invalid signature", async () => { const req = makeRequest(CHARGE_SUCCESS, "badsignature"); const res = await POST(req); @@ -42,24 +52,42 @@ describe("POST /api/webhooks/paystack", () => { }); it("returns 200 and skips non-charge.success events", async () => { - const req = makeRequest({ event: "transfer.success", data: {} }); + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // processed_webhooks check + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // record non-success event + + const req = makeRequest({ id: "evt_456", event: "transfer.success", data: {} }); const res = await POST(req); expect(res.status).toBe(200); const json = await res.json(); expect(json.received).toBe(true); - expect(mockQuery).not.toHaveBeenCalled(); + + // Should check if already processed + expect(mockQuery).toHaveBeenCalledWith( + expect.stringContaining("FROM processed_webhooks"), + ["evt_456"] + ); + // Should record the non-success event + expect(mockQuery).toHaveBeenCalledWith( + expect.stringContaining("INSERT INTO processed_webhooks"), + ["evt_456", "transfer.success", expect.any(Object)] + ); }); it("returns 400 when reference is missing", async () => { - const req = makeRequest({ event: "charge.success", data: {} }); + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // processed_webhooks check + const req = makeRequest({ id: "evt_789", event: "charge.success", data: {} }); const res = await POST(req); expect(res.status).toBe(400); }); it("confirms contribution on charge.success using paystack_reference", async () => { - mockQuery - .mockResolvedValueOnce({ rows: [], rowCount: 0 } as any) - .mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // processed_webhooks check + + const mockTxQuery = jest.fn() + .mockResolvedValueOnce({ rows: [], rowCount: 1 } as any) // insert processed_webhooks + .mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // update contributions + + mockTransaction.mockImplementationOnce(async (cb) => cb(mockTxQuery)); const req = makeRequest(CHARGE_SUCCESS); const res = await POST(req); @@ -67,22 +95,25 @@ describe("POST /api/webhooks/paystack", () => { const json = await res.json(); expect(json.received).toBe(true); - // Idempotency check uses paystack_reference - expect(mockQuery).toHaveBeenNthCalledWith( - 1, - expect.stringContaining("paystack_reference = $1"), - ["ajo-circle-1-member-1-2"] + // Initial check + expect(mockQuery).toHaveBeenCalledWith( + expect.stringContaining("FROM processed_webhooks"), + ["evt_123"] + ); + + // Transactional steps + expect(mockTxQuery).toHaveBeenCalledWith( + expect.stringContaining("INSERT INTO processed_webhooks"), + ["evt_123", "charge.success", expect.any(Object)] ); - // Confirm update uses paystack_reference - expect(mockQuery).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("paystack_reference = $1"), + expect(mockTxQuery).toHaveBeenCalledWith( + expect.stringContaining("UPDATE contributions"), ["ajo-circle-1-member-1-2"] ); }); - it("returns duplicate:true and skips update for already-processed reference", async () => { - mockQuery.mockResolvedValueOnce({ rows: [{ id: "contrib-1" }], rowCount: 1 } as any); + it("returns duplicate:true and skips update for already-processed event ID", async () => { + mockQuery.mockResolvedValueOnce({ rows: [{ id: "evt_123" }], rowCount: 1 } as any); const req = makeRequest(CHARGE_SUCCESS); const res = await POST(req); @@ -90,5 +121,6 @@ describe("POST /api/webhooks/paystack", () => { const json = await res.json(); expect(json.duplicate).toBe(true); expect(mockQuery).toHaveBeenCalledTimes(1); + expect(mockTransaction).not.toHaveBeenCalled(); }); }); diff --git a/src/app/api/webhooks/paystack/__tests__/route.test.ts b/src/app/api/webhooks/paystack/__tests__/route.test.ts deleted file mode 100644 index 47b1729..0000000 --- a/src/app/api/webhooks/paystack/__tests__/route.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @jest-environment node - */ -import { POST } from "@/app/api/webhooks/paystack/route"; -import { NextRequest } from "next/server"; -import { createHmac } from "crypto"; - -jest.mock("@/lib/db", () => ({ - query: jest.fn(), - transaction: jest.fn((cb) => cb(jest.fn())), -})); -jest.mock("@/server/config", () => ({ - serverConfig: { paystack: { secretKey: "test-secret" } }, -})); -jest.mock("@/lib/logger", () => ({ - warn: jest.fn(), - info: jest.fn(), - error: jest.fn(), -})); - -import * as db from "@/lib/db"; -const mockQuery = db.query as jest.MockedFunction; -const mockTransaction = db.transaction as jest.MockedFunction; - -const SECRET = "test-secret"; - -function makeRequest(body: object, signature?: string): NextRequest { - const raw = JSON.stringify(body); - const sig = - signature ?? - createHmac("sha512", SECRET).update(raw).digest("hex"); - return new NextRequest("http://localhost/api/webhooks/paystack", { - method: "POST", - headers: { "x-paystack-signature": sig, "content-type": "application/json" }, - body: raw, - }); -} - -const CHARGE_SUCCESS = { - id: "evt_123", - event: "charge.success", - data: { reference: "ajo-circle-1-member-1-2" }, -}; - -beforeEach(() => jest.clearAllMocks()); - -describe("POST /api/webhooks/paystack", () => { - it("returns 401 for invalid signature", async () => { - const req = makeRequest(CHARGE_SUCCESS, "badsignature"); - const res = await POST(req); - expect(res.status).toBe(401); - }); - - it("returns 200 and skips non-charge.success events", async () => { - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // processed_webhooks check - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // record non-success event - - const req = makeRequest({ id: "evt_456", event: "transfer.success", data: {} }); - const res = await POST(req); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.received).toBe(true); - - // Should check if already processed - expect(mockQuery).toHaveBeenCalledWith( - expect.stringContaining("FROM processed_webhooks"), - ["evt_456"] - ); - // Should record the non-success event - expect(mockQuery).toHaveBeenCalledWith( - expect.stringContaining("INSERT INTO processed_webhooks"), - ["evt_456", "transfer.success", expect.any(Object)] - ); - }); - - it("returns 400 when reference is missing", async () => { - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // processed_webhooks check - const req = makeRequest({ id: "evt_789", event: "charge.success", data: {} }); - const res = await POST(req); - expect(res.status).toBe(400); - }); - - it("confirms contribution on charge.success using paystack_reference", async () => { - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); // processed_webhooks check - - const mockTxQuery = jest.fn() - .mockResolvedValueOnce({ rows: [], rowCount: 1 } as any) // insert processed_webhooks - .mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // update contributions - - mockTransaction.mockImplementationOnce(async (cb) => cb(mockTxQuery)); - - const req = makeRequest(CHARGE_SUCCESS); - const res = await POST(req); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.received).toBe(true); - - // Initial check - expect(mockQuery).toHaveBeenCalledWith( - expect.stringContaining("FROM processed_webhooks"), - ["evt_123"] - ); - - // Transactional steps - expect(mockTxQuery).toHaveBeenCalledWith( - expect.stringContaining("INSERT INTO processed_webhooks"), - ["evt_123", "charge.success", expect.any(Object)] - ); - expect(mockTxQuery).toHaveBeenCalledWith( - expect.stringContaining("UPDATE contributions"), - ["ajo-circle-1-member-1-2"] - ); - }); - - it("returns duplicate:true and skips update for already-processed event ID", async () => { - mockQuery.mockResolvedValueOnce({ rows: [{ id: "evt_123" }], rowCount: 1 } as any); - - const req = makeRequest(CHARGE_SUCCESS); - const res = await POST(req); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.duplicate).toBe(true); - expect(mockQuery).toHaveBeenCalledTimes(1); - expect(mockTransaction).not.toHaveBeenCalled(); - }); -}); diff --git a/src/app/api/webhooks/paystack/route.ts b/src/app/api/webhooks/paystack/route.ts deleted file mode 100644 index a59c99a..0000000 --- a/src/app/api/webhooks/paystack/route.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { createHmac, timingSafeEqual } from "crypto"; -import { query, transaction } from "@/lib/db"; -import { serverConfig } from "@/server/config"; -import logger from "@/lib/logger"; - -function verifySignature(payload: string, signature: string): boolean { - if (!signature || !serverConfig.paystack.secretKey) return false; - - const expected = createHmac("sha512", serverConfig.paystack.secretKey) - .update(payload) - .digest("hex"); - - const expectedBuffer = Buffer.from(expected); - const signatureBuffer = Buffer.from(signature); - - if (expectedBuffer.length !== signatureBuffer.length) { - return false; - } - - return timingSafeEqual(expectedBuffer, signatureBuffer); -} - -export async function POST(req: NextRequest) { - const signature = req.headers.get("x-paystack-signature") ?? ""; - const rawBody = await req.text(); - - if (!verifySignature(rawBody, signature)) { - logger.warn({ signature }, "Invalid Paystack webhook signature"); - return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); - } - - const event = JSON.parse(rawBody); - const eventId = event.id?.toString() || event.data?.id?.toString(); - - if (!eventId) { - logger.error({ event }, "Paystack webhook missing event ID"); - return NextResponse.json({ error: "Missing event ID" }, { status: 400 }); - } - - // Replay attack prevention: check if event already processed - const { rows: existingEvent } = await query( - "SELECT id FROM processed_webhooks WHERE id = $1 AND provider = 'paystack'", - [eventId] - ); - - if (existingEvent.length > 0) { - logger.info({ eventId }, "Paystack webhook already processed"); - return NextResponse.json({ received: true, duplicate: true }); - } - - logger.info({ eventId, event: event.event }, "Paystack webhook verified"); - - if (event.event !== "charge.success") { - // Record non-charge.success events too to prevent replays - await query( - "INSERT INTO processed_webhooks (id, provider, event_type, payload) VALUES ($1, 'paystack', $2, $3)", - [eventId, event.event, event] - ); - return NextResponse.json({ received: true }); - } - - const reference: string = event.data?.reference; - if (!reference) { - return NextResponse.json({ error: "Missing reference" }, { status: 400 }); - } - - try { - await transaction(async (q) => { - // Record the webhook as processed within the transaction - await q( - "INSERT INTO processed_webhooks (id, provider, event_type, payload) VALUES ($1, 'paystack', $2, $3)", - [eventId, event.event, event] - ); - - // Confirm the pending contribution matching this paystack_reference - const { rowCount } = await q( - `UPDATE contributions - SET status = 'confirmed', tx_hash = $1, updated_at = NOW() - WHERE paystack_reference = $1 AND status = 'pending'`, - [reference] - ); - - if (rowCount === 0) { - logger.info({ reference }, "Paystack reference not found or already confirmed"); - } else { - logger.info({ reference }, "Contribution confirmed via Paystack webhook"); - } - }); - - return NextResponse.json({ received: true }); - } catch (err) { - logger.error({ err, eventId }, "Error processing Paystack webhook"); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 07cc95c..872608f 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -50,7 +50,7 @@ export default function LoginPage() { const sendOtp = async () => { setLoading(true); setError(null); try { - const res = await fetch("/api/auth/send-otp", { + const res = await fetch("/api/v1/auth/send-otp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: fullPhone }), diff --git a/src/app/circles/[id]/contribute/callback/page.tsx b/src/app/circles/[id]/contribute/callback/page.tsx index d275a36..9fc7f60 100644 --- a/src/app/circles/[id]/contribute/callback/page.tsx +++ b/src/app/circles/[id]/contribute/callback/page.tsx @@ -16,7 +16,7 @@ export default function ContributeCallbackPage() { useEffect(() => { if (!reference) { setStatus("failed"); return; } - fetch(`/api/circles/${params.id}/contribute/verify?reference=${reference}`) + fetch(`/api/v1/circles/${params.id}/contribute/verify?reference=${reference}`) .then((r) => r.json()) .then((json) => { setStatus(json.success && json.data.status === "success" ? "success" : "failed"); diff --git a/src/app/circles/[id]/page.tsx b/src/app/circles/[id]/page.tsx index 2309a84..01e8b20 100644 --- a/src/app/circles/[id]/page.tsx +++ b/src/app/circles/[id]/page.tsx @@ -13,6 +13,7 @@ import { format } from "date-fns"; import type { Metadata } from "next"; import { CircleChat } from "@/components/circle/CircleChat"; import { CircleWaitlist } from "@/components/circle/CircleWaitlist"; +import { CopyButton } from "@/components/ui/CopyButton"; import styles from "./page.module.css"; interface Props { diff --git a/src/app/manifest.ts b/src/app/manifest.ts index cd0cb15..9f02f6b 100644 --- a/src/app/manifest.ts +++ b/src/app/manifest.ts @@ -11,8 +11,8 @@ export default function manifest(): MetadataRoute.Manifest { theme_color: "#0f7a4a", orientation: "portrait", icons: [ - { src: "/icons/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "any maskable" }, - { src: "/icons/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "any maskable" }, + { src: "/icons/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "any maskable" as any }, + { src: "/icons/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "any maskable" as any }, ], categories: ["finance", "utilities"], screenshots: [], diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index ca69c09..8db4c94 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -5,7 +5,7 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import styles from "./page.module.css"; import type { ProfileData } from "@/app/api/v1/profile/route"; -import type { ReferralData } from "@/app/api/referral/route"; +import type { ReferralData } from "@/app/api/v1/referral/route"; import { useFreighterWallet } from "@/hooks/useFreighterWallet"; import { ConnectWalletButton } from "@/components/wallet/ConnectWalletButton"; @@ -46,7 +46,7 @@ export default function ProfilePage() { }); } }); - fetch("/api/referral") + fetch("/api/v1/referral") .then((r) => r.json()) .then((json) => { if (json.success) setReferral(json.data); }); }, [status]); @@ -63,7 +63,7 @@ export default function ProfilePage() { useEffect(() => { const key = profile?.stellarPublicKey; if (!key) { setUsdcBalance(null); setHasUsdcTrustline(null); return; } - fetch(`/api/stellar/balance?publicKey=${encodeURIComponent(key)}`) + fetch(`/api/v1/stellar/balance?publicKey=${encodeURIComponent(key)}`) .then((r) => r.json()) .then((json) => { if (json.success) { @@ -87,7 +87,7 @@ export default function ProfilePage() { setApplyingCode(true); setReferralMsg(null); try { - const res = await fetch("/api/referral", { + const res = await fetch("/api/v1/referral", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: referralCode }), diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index dc587bf..7360b2f 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -17,7 +17,7 @@ export default function SettingsPage() { setMessage(null); try { - const res = await fetch("/api/user/sms-preferences", { + const res = await fetch("/api/v1/user/sms-preferences", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: !smsEnabled }), @@ -115,7 +115,7 @@ export default function SettingsPage() {
Phone - {session.user?.phone || "Not set"} + {(session.user as { phone?: string })?.phone || "Not set"}
Display Name diff --git a/src/components/admin/AdminDashboard.tsx b/src/components/admin/AdminDashboard.tsx index 32bfc50..6af07c9 100644 --- a/src/components/admin/AdminDashboard.tsx +++ b/src/components/admin/AdminDashboard.tsx @@ -23,7 +23,7 @@ export function AdminDashboard() { // Fetch function for circles const fetchCircles = useCallback(async () => { - const res = await fetch("/api/admin/circles"); + const res = await fetch("/api/v1/admin/circles"); const json = await res.json(); if (!json.success) throw new Error(json.error); return json.data as AdminCircleRow[]; @@ -31,7 +31,7 @@ export function AdminDashboard() { // Fetch function for payouts const fetchPayouts = useCallback(async () => { - const res = await fetch("/api/admin/payouts"); + const res = await fetch("/api/v1/admin/payouts"); const json = await res.json(); if (!json.success) throw new Error(json.error); return json.data as AdminPayoutRow[]; diff --git a/src/components/admin/AnalyticsDashboard.tsx b/src/components/admin/AnalyticsDashboard.tsx index e5cfe10..706ac52 100644 --- a/src/components/admin/AnalyticsDashboard.tsx +++ b/src/components/admin/AnalyticsDashboard.tsx @@ -49,7 +49,7 @@ export function AnalyticsDashboard() { useEffect(() => { async function fetchAnalytics() { try { - const res = await fetch("/api/admin/analytics"); + const res = await fetch("/api/v1/admin/analytics"); const json = await res.json(); if (!json.success) { throw new Error(json.error || "Failed to fetch analytics"); @@ -201,7 +201,7 @@ export function AnalyticsDashboard() { // Handle export trigger const handleExport = () => { - window.open("/api/admin/analytics/export", "_blank"); + window.open("/api/v1/admin/analytics/export", "_blank"); }; return ( diff --git a/src/components/admin/CirclesTable.tsx b/src/components/admin/CirclesTable.tsx index 608c18f..64d173c 100644 --- a/src/components/admin/CirclesTable.tsx +++ b/src/components/admin/CirclesTable.tsx @@ -26,7 +26,7 @@ export function CirclesTable({ circles }: CirclesTableProps) { setPayoutSuccess(null); try { - const res = await fetch(`/api/admin/circles/${circleId}/payout`, { method: "POST" }); + const res = await fetch(`/api/v1/admin/circles/${circleId}/payout`, { method: "POST" }); const json = await res.json(); if (!json.success) throw new Error(json.error); diff --git a/src/components/admin/DisputeList.tsx b/src/components/admin/DisputeList.tsx index 4dba003..ec94137 100644 --- a/src/components/admin/DisputeList.tsx +++ b/src/components/admin/DisputeList.tsx @@ -23,7 +23,7 @@ export function DisputeList({ disputes }: DisputeListProps) { setResolving(disputeId); try { - const res = await fetch("/api/admin/disputes", { + const res = await fetch("/api/v1/admin/disputes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/src/components/circle/CircleActions.tsx b/src/components/circle/CircleActions.tsx index df9d20b..5cfaae5 100644 --- a/src/components/circle/CircleActions.tsx +++ b/src/components/circle/CircleActions.tsx @@ -23,7 +23,7 @@ export function CircleActions({ circleId, isCreator, isMember, status }: Props) setLoading(true); setError(null); try { - const res = await fetch(`/api/circles/${circleId}/invite`); + const res = await fetch(`/api/v1/circles/${circleId}/invite`); const json = await res.json(); if (!json.success) throw new Error(json.error); @@ -41,7 +41,7 @@ export function CircleActions({ circleId, isCreator, isMember, status }: Props) setLoading(true); setError(null); try { - const res = await fetch(`/api/circles/${circleId}/cancel`, { method: "POST" }); + const res = await fetch(`/api/v1/circles/${circleId}/cancel`, { method: "POST" }); const json = await res.json(); if (!json.success) throw new Error(json.error); router.refresh(); @@ -57,7 +57,7 @@ export function CircleActions({ circleId, isCreator, isMember, status }: Props) setLoading(true); setError(null); try { - const res = await fetch(`/api/circles/${circleId}/leave`, { method: "POST" }); + const res = await fetch(`/api/v1/circles/${circleId}/leave`, { method: "POST" }); const json = await res.json(); if (!json.success) throw new Error(json.error); router.push("/circles"); @@ -73,7 +73,7 @@ export function CircleActions({ circleId, isCreator, isMember, status }: Props) setLoading(true); setError(null); try { - const res = await fetch(`/api/circles/${circleId}/pause`, { method: "POST" }); + const res = await fetch(`/api/v1/circles/${circleId}/pause`, { method: "POST" }); const json = await res.json(); if (!json.success) throw new Error(json.error); router.refresh(); @@ -89,7 +89,7 @@ export function CircleActions({ circleId, isCreator, isMember, status }: Props) setLoading(true); setError(null); try { - const res = await fetch(`/api/circles/${circleId}/resume`, { method: "POST" }); + const res = await fetch(`/api/v1/circles/${circleId}/resume`, { method: "POST" }); const json = await res.json(); if (!json.success) throw new Error(json.error); router.refresh(); @@ -113,7 +113,7 @@ export function CircleActions({ circleId, isCreator, isMember, status }: Props)
{isCreator && status === "open" && (