diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9baf066 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm add *)", + "Bash(node --experimental-vm-modules /tmp/ip-probe.mjs)", + "Bash(cat > *)", + "Bash(node /tmp/ip-probe.cjs)", + "Bash(pnpm exec *)", + "Bash(pnpm test *)", + "Bash(git stash *)", + "Bash(git diff *)", + "Bash(git add *)", + "Bash(echo \"exit: $?\")" + ], + "additionalDirectories": [ + "/private/tmp" + ] + } +} diff --git a/.env.example b/.env.example index 53911d1..6cf1885 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,8 @@ JWT_SECRET= # Blockchain RPC_URL= TOKEN_TRANSFER_CONTRACT_ID= - +GROUP_TREASURY_CONTRACT_ID= +PROPOSALS_CONTRACT_ID= # Database DATABASE_URL= diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 0000000..e3a40c9 --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,47 @@ +name: Backend CI + +on: + push: + paths: + - 'apps/backend/**' + - '.github/workflows/backend-ci.yml' + pull_request: + paths: + - 'apps/backend/**' + - '.github/workflows/backend-ci.yml' + +jobs: + check: + name: Format · Lint · Test + runs-on: ubuntu-latest + + defaults: + run: + working-directory: apps/backend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: . + + - name: Format check + run: pnpm format:check + + - name: Lint + run: pnpm lint + + - name: Tests + run: pnpm test + env: + JWT_SECRET: ${{ secrets.JWT_SECRET || 'ci-test-secret' }} diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml new file mode 100644 index 0000000..b54cc2b --- /dev/null +++ b/.github/workflows/contracts-ci.yml @@ -0,0 +1,55 @@ +# Issue #48 — Contracts CI for the Soroban Rust workspace. +# +# Triggers on push or PR that touches `contracts/**` (or this workflow file). +# Installs the pinned Rust toolchain and the `wasm32-unknown-unknown` target, +# runs `cargo test -p token_transfer`, then builds the release WASM. Cargo's +# registry and `target/` directory are cached on the workflow key so +# subsequent runs skip the dependency rebuild. + +name: Contracts CI + +on: + push: + paths: + - 'contracts/**' + - '.github/workflows/contracts-ci.yml' + pull_request: + paths: + - 'contracts/**' + - '.github/workflows/contracts-ci.yml' + +jobs: + test-and-build: + name: cargo test + build (wasm32) + runs-on: ubuntu-latest + + defaults: + run: + working-directory: contracts + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain (stable + wasm32 target) + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache cargo registry and build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + contracts/target + key: ${{ runner.os }}-cargo-contracts-${{ hashFiles('contracts/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-contracts- + + - name: cargo test -p token_transfer + run: cargo test -p token_transfer + + - name: cargo build (release wasm32) + run: cargo build -p token_transfer --target wasm32-unknown-unknown --release diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 0000000..cdcf7e2 --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,37 @@ +name: Frontend CI + +on: + push: + paths: + - 'apps/web/**' + - '.github/workflows/frontend-ci.yml' + pull_request: + paths: + - 'apps/web/**' + - '.github/workflows/frontend-ci.yml' + +jobs: + check: + name: Lint · Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm --filter web lint + + - name: Build + run: pnpm --filter web build diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c125a2c..cb01bf1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,4 +16,4 @@ jobs: - name: Install dependencies run: npm i -g pnpm && pnpm install - name: Lint - run: pnpm run lint --if-present + run: pnpm run lint diff --git a/.gitignore b/.gitignore index e3fab13..5702e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ __pycache__/ target/ .soroban/ .DS_Store -.turbo/ \ No newline at end of file +.turbo/ + +ISSUES.md +IMPLEMENTATION_DOCS.md \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4cbc711 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..13e108b --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +# Start infrastructure (Postgres, Redis) then all applications via Turborepo. +dev: + docker compose -f infra/docker-compose.yml up -d + pnpm dev + +# Run database migrations for the backend package. +migrate: + pnpm --filter backend db:migrate + +# Run all test suites: JS/TS packages and Soroban contracts. +test: + pnpm --filter backend test + cd contracts && cargo test + +# Run linting across all packages via Turborepo. +lint: + pnpm lint + +# Build and deploy all Soroban smart contracts to the configured network. +deploy-contracts: + bash contracts/scripts/deploy_token_transfer.sh + bash contracts/scripts/deploy_group_treasury.sh + +.PHONY: dev migrate test lint deploy-contracts diff --git a/apps/ai_agent/main.py b/apps/ai_agent/main.py index 31350a3..e11a925 100644 --- a/apps/ai_agent/main.py +++ b/apps/ai_agent/main.py @@ -1,11 +1,261 @@ -from fastapi import FastAPI +import json +import os +from typing import Literal + import uvicorn +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +import weaviate +from weaviate.classes.query import Filter app = FastAPI(title="AI Agent API") +_SYSTEM_PROMPT = ( + "You are an AI assistant for Clicked, a decentralised messaging and payment " + "platform built on the Stellar blockchain. Clicked lets users send token " + "payments inside chat conversations, manage group treasuries, and participate " + "in DAO-style governance. Help users with questions about transactions, wallet " + "management, group finances, and platform features." +) + +_HIGH_VALUE_THRESHOLD = 10_000.0 + + +# ── Request / response models ───────────────────────────────────────────────── + +class ChatRequest(BaseModel): + message: str + conversation_id: str + + +class ChatResponse(BaseModel): + reply: str + + +class TransferAnalyseRequest(BaseModel): + amount: float + sender: str + recipient: str + memo: str + + +class TransferAnalyseResponse(BaseModel): + flagged: bool + reason: str | None + confidence: float + + +class IndexMessageRequest(BaseModel): + messageId: str + conversationId: str + senderId: str + content: str + + +RiskLevel = Literal["low", "medium", "high"] + + +class ProposalSummariseRequest(BaseModel): + title: str + description: str + amount: float + + +class ProposalSummariseResponse(BaseModel): + summary: str + risk: RiskLevel + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def _openai_client(): + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise HTTPException(status_code=500, detail="OPENAI_API_KEY is not configured") + from openai import OpenAI # imported lazily so missing package gives a clear error + return OpenAI(api_key=api_key) + + +# ── Endpoints ───────────────────────────────────────────────────────────────── + @app.get("/health") def health_check(): return {"status": "ok"} + +@app.post("/chat", response_model=ChatResponse) +def chat(request: ChatRequest): + client = _openai_client() + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": _SYSTEM_PROMPT}, + {"role": "user", "content": request.message}, + ], + timeout=30, + ) + return ChatResponse(reply=response.choices[0].message.content) + + +@app.post("/transfers/analyse", response_model=TransferAnalyseResponse) +def analyse_transfer(request: TransferAnalyseRequest): + # Rule-based: high-value transfers are always flagged with high confidence. + if request.amount > _HIGH_VALUE_THRESHOLD: + return TransferAnalyseResponse( + flagged=True, + reason=f"Amount {request.amount} XLM exceeds {_HIGH_VALUE_THRESHOLD} XLM threshold", + confidence=0.99, + ) + + client = _openai_client() + prompt = ( + "Analyse this Stellar transfer for fraud risk.\n" + f"Amount: {request.amount} XLM\n" + f"Sender: {request.sender}\n" + f"Recipient: {request.recipient}\n" + f"Memo: {request.memo}\n\n" + "Reply with JSON only using keys: flagged (bool), reason (string under 100 chars or null), " + "confidence (float 0-1). Flag if suspicious patterns are detected." + ) + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + timeout=10, + ) + result = json.loads(response.choices[0].message.content) + return TransferAnalyseResponse( + flagged=bool(result.get("flagged", False)), + reason=result.get("reason"), + confidence=float(result.get("confidence", 0.0)), + ) + + +@app.post("/proposals/summarise", response_model=ProposalSummariseResponse) +def summarise_proposal(request: ProposalSummariseRequest): + client = _openai_client() + prompt = ( + "Summarise this Clicked governance proposal for a frontend reader and " + "rate its risk level.\n" + f"Title: {request.title}\n" + f"Description: {request.description}\n" + f"Amount: {request.amount} XLM\n\n" + "Reply with JSON only using keys: summary (a plain-English summary of " + "exactly 2 sentences), risk (one of \"low\", \"medium\", \"high\"). " + "Use \"high\" for large amounts, unclear intent, or obvious red flags; " + "\"low\" for small, well-scoped, low-impact proposals; otherwise \"medium\"." + ) + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + timeout=10, + ) + result = json.loads(response.choices[0].message.content) + + summary = (result.get("summary") or "").strip() + if not summary: + raise HTTPException(status_code=502, detail="LLM did not return a summary") + + risk = str(result.get("risk", "")).strip().lower() + if risk not in ("low", "medium", "high"): + # Defensive fallback: never return an invalid risk level to the caller. + risk = "medium" + + # Pydantic re-validates via response_model before the response is sent. + return ProposalSummariseResponse(summary=summary, risk=risk) + + +@app.post("/index/message") +def index_message(request: IndexMessageRequest): + try: + # Attempt connection to Weaviate + client = weaviate.connect_to_local() + except Exception as e: + raise HTTPException(status_code=503, detail="Weaviate connection failed") + + try: + if not client.collections.exists("Message"): + client.collections.create(name="Message") + + collection = client.collections.get("Message") + + # Get embedding via OpenAI + openai_client = _openai_client() + res = openai_client.embeddings.create(input=request.content, model="text-embedding-3-small") + vector = res.data[0].embedding + + # Upsert + if collection.data.exists(request.messageId): + collection.data.replace( + uuid=request.messageId, + properties={ + "conversationId": request.conversationId, + "messageId": request.messageId, + "senderId": request.senderId, + "content": request.content, + }, + vector=vector + ) + else: + collection.data.insert( + uuid=request.messageId, + properties={ + "conversationId": request.conversationId, + "messageId": request.messageId, + "senderId": request.senderId, + "content": request.content, + }, + vector=vector + ) + except Exception as e: + raise HTTPException(status_code=503, detail=str(e)) + finally: + client.close() + + return {"status": "ok"} + + +@app.get("/search") +def search_messages(q: str, conversationId: str): + try: + client = weaviate.connect_to_local() + except Exception as e: + raise HTTPException(status_code=503, detail="Weaviate connection failed") + + try: + if not client.collections.exists("Message"): + return {"results": []} + + collection = client.collections.get("Message") + + # Get embedding for query + openai_client = _openai_client() + res = openai_client.embeddings.create(input=q, model="text-embedding-3-small") + vector = res.data[0].embedding + + results = collection.query.near_vector( + near_vector=vector, + limit=5, + filters=Filter.by_property("conversationId").equal(conversationId) + ) + + hits = [] + for obj in results.objects: + hits.append({ + "messageId": obj.properties.get("messageId"), + "conversationId": obj.properties.get("conversationId"), + "senderId": obj.properties.get("senderId"), + "content": obj.properties.get("content"), + }) + + return {"results": hits} + except Exception as e: + raise HTTPException(status_code=503, detail=str(e)) + finally: + client.close() + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/apps/ai_agent/pyproject.toml b/apps/ai_agent/pyproject.toml index f4bbed5..080f8d8 100644 --- a/apps/ai_agent/pyproject.toml +++ b/apps/ai_agent/pyproject.toml @@ -7,4 +7,6 @@ requires-python = ">=3.12" dependencies = [ "fastapi>=0.135.1", "uvicorn>=0.42.0", + "openai>=1.0.0", + "weaviate-client>=4.0.0", ] diff --git a/apps/backend/drizzle/0002_greedy_hellion.sql b/apps/backend/drizzle/0002_greedy_hellion.sql new file mode 100644 index 0000000..1852472 --- /dev/null +++ b/apps/backend/drizzle/0002_greedy_hellion.sql @@ -0,0 +1,2 @@ +ALTER TABLE "conversation_members" ADD COLUMN "last_read_message_id" uuid;--> statement-breakpoint +ALTER TABLE "conversation_members" ADD CONSTRAINT "conversation_members_last_read_message_id_messages_id_fk" FOREIGN KEY ("last_read_message_id") REFERENCES "public"."messages"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/apps/backend/drizzle/0003_messages_content_search_idx.sql b/apps/backend/drizzle/0003_messages_content_search_idx.sql new file mode 100644 index 0000000..494b906 --- /dev/null +++ b/apps/backend/drizzle/0003_messages_content_search_idx.sql @@ -0,0 +1 @@ +CREATE INDEX "messages_content_search_idx" ON "messages" USING gin (to_tsvector('english', "content")); diff --git a/apps/backend/drizzle/0004_premium_mattie_franklin.sql b/apps/backend/drizzle/0004_premium_mattie_franklin.sql new file mode 100644 index 0000000..0f58877 --- /dev/null +++ b/apps/backend/drizzle/0004_premium_mattie_franklin.sql @@ -0,0 +1,15 @@ +CREATE TABLE "token_transfers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "conversation_id" uuid NOT NULL, + "sender_id" uuid NOT NULL, + "recipient_address" text NOT NULL, + "amount" text NOT NULL, + "token_contract_id" text NOT NULL, + "tx_hash" text NOT NULL, + "memo" text, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "token_transfers_tx_hash_unique" UNIQUE("tx_hash") +); +--> statement-breakpoint +ALTER TABLE "token_transfers" ADD CONSTRAINT "token_transfers_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "token_transfers" ADD CONSTRAINT "token_transfers_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/backend/drizzle/0005_conversation_member_settings.sql b/apps/backend/drizzle/0005_conversation_member_settings.sql new file mode 100644 index 0000000..d9141f7 --- /dev/null +++ b/apps/backend/drizzle/0005_conversation_member_settings.sql @@ -0,0 +1,2 @@ +ALTER TABLE "conversation_members" ADD COLUMN "is_muted" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "conversation_members" ADD COLUMN "is_archived" boolean DEFAULT false NOT NULL; diff --git a/apps/backend/drizzle/0005_soft_deleted_messages.sql b/apps/backend/drizzle/0005_soft_deleted_messages.sql new file mode 100644 index 0000000..fa01971 --- /dev/null +++ b/apps/backend/drizzle/0005_soft_deleted_messages.sql @@ -0,0 +1 @@ +ALTER TABLE "messages" ADD COLUMN "deleted_at" timestamp; \ No newline at end of file diff --git a/apps/backend/drizzle/0006_add_conversation_avatar_url.sql b/apps/backend/drizzle/0006_add_conversation_avatar_url.sql new file mode 100644 index 0000000..e05d59a --- /dev/null +++ b/apps/backend/drizzle/0006_add_conversation_avatar_url.sql @@ -0,0 +1 @@ +ALTER TABLE "conversations" ADD COLUMN "avatar_url" text; diff --git a/apps/backend/drizzle/0007_device_key_bundles.sql b/apps/backend/drizzle/0007_device_key_bundles.sql new file mode 100644 index 0000000..3a7eec8 --- /dev/null +++ b/apps/backend/drizzle/0007_device_key_bundles.sql @@ -0,0 +1,29 @@ +CREATE TABLE "devices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "identity_public_key" text NOT NULL, + "registration_id" integer NOT NULL, + "signed_pre_key_id" integer NOT NULL, + "signed_pre_key_public" text NOT NULL, + "signed_pre_key_signature" text NOT NULL, + "revoked_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "one_time_pre_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "key_id" integer NOT NULL, + "public_key" text NOT NULL, + "consumed" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "one_time_pre_keys_device_key_unique" UNIQUE("device_id","key_id") +); +--> statement-breakpoint +ALTER TABLE "devices" ADD CONSTRAINT "devices_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "one_time_pre_keys" ADD CONSTRAINT "one_time_pre_keys_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +CREATE INDEX "devices_user_id_idx" ON "devices" USING btree ("user_id"); +--> statement-breakpoint +CREATE INDEX "one_time_pre_keys_device_consumed_idx" ON "one_time_pre_keys" USING btree ("device_id","consumed"); diff --git a/apps/backend/drizzle/meta/0002_snapshot.json b/apps/backend/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..88c537b --- /dev/null +++ b/apps/backend/drizzle/meta/0002_snapshot.json @@ -0,0 +1,348 @@ +{ + "id": "dce54807-ba05-4ac2-bb4c-c58334c2a707", + "prevId": "f0633220-541a-4c99-9b99-8242c7f3420f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.conversation_members": { + "name": "conversation_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_message_id": { + "name": "last_read_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "conversation_members_conversation_id_conversations_id_fk": { + "name": "conversation_members_conversation_id_conversations_id_fk", + "tableFrom": "conversation_members", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_members_user_id_users_id_fk": { + "name": "conversation_members_user_id_users_id_fk", + "tableFrom": "conversation_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_members_last_read_message_id_messages_id_fk": { + "name": "conversation_members_last_read_message_id_messages_id_fk", + "tableFrom": "conversation_members", + "tableTo": "messages", + "columnsFrom": [ + "last_read_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "conversation_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dm'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_sender_id_users_id_fk": { + "name": "messages_sender_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallets": { + "name": "wallets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "wallets_user_id_users_id_fk": { + "name": "wallets_user_id_users_id_fk", + "tableFrom": "wallets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wallets_address_unique": { + "name": "wallets_address_unique", + "nullsNotDistinct": false, + "columns": [ + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.conversation_type": { + "name": "conversation_type", + "schema": "public", + "values": [ + "dm", + "group" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0004_snapshot.json b/apps/backend/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..ba18857 --- /dev/null +++ b/apps/backend/drizzle/meta/0004_snapshot.json @@ -0,0 +1,468 @@ +{ + "id": "dfbe295d-f8b1-462b-9ad9-e23c4e3fefd2", + "prevId": "dce54807-ba05-4ac2-bb4c-c58334c2a707", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.conversation_members": { + "name": "conversation_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_message_id": { + "name": "last_read_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "conversation_members_conversation_id_conversations_id_fk": { + "name": "conversation_members_conversation_id_conversations_id_fk", + "tableFrom": "conversation_members", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_members_user_id_users_id_fk": { + "name": "conversation_members_user_id_users_id_fk", + "tableFrom": "conversation_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_members_last_read_message_id_messages_id_fk": { + "name": "conversation_members_last_read_message_id_messages_id_fk", + "tableFrom": "conversation_members", + "tableTo": "messages", + "columnsFrom": [ + "last_read_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "conversation_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dm'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_content_search_idx": { + "name": "messages_content_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"content\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_sender_id_users_id_fk": { + "name": "messages_sender_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_transfers": { + "name": "token_transfers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_address": { + "name": "recipient_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_contract_id": { + "name": "token_contract_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "token_transfers_conversation_id_conversations_id_fk": { + "name": "token_transfers_conversation_id_conversations_id_fk", + "tableFrom": "token_transfers", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "token_transfers_sender_id_users_id_fk": { + "name": "token_transfers_sender_id_users_id_fk", + "tableFrom": "token_transfers", + "tableTo": "users", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "token_transfers_tx_hash_unique": { + "name": "token_transfers_tx_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "tx_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallets": { + "name": "wallets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "wallets_user_id_users_id_fk": { + "name": "wallets_user_id_users_id_fk", + "tableFrom": "wallets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wallets_address_unique": { + "name": "wallets_address_unique", + "nullsNotDistinct": false, + "columns": [ + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.conversation_type": { + "name": "conversation_type", + "schema": "public", + "values": [ + "dm", + "group" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index 233d680..1baaa94 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -15,6 +15,48 @@ "when": 1778597011664, "tag": "0001_messaging", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1780063933769, + "tag": "0002_greedy_hellion", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1780129152703, + "tag": "0003_messages_content_search_idx", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1780241403332, + "tag": "0004_premium_mattie_franklin", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1780269667000, + "tag": "0005_soft_deleted_messages", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1780560000000, + "tag": "0006_add_conversation_avatar_url", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1780646400000, + "tag": "0007_device_key_bundles", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/backend/eslint.config.js b/apps/backend/eslint.config.js new file mode 100644 index 0000000..8665cda --- /dev/null +++ b/apps/backend/eslint.config.js @@ -0,0 +1,17 @@ +// @ts-check +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, + { + ignores: ['dist/', 'node_modules/'], + }, +); diff --git a/apps/backend/package.json b/apps/backend/package.json index 9c856a6..5d8b3bd 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -5,7 +5,13 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "format:check": "prettier --check \"src/**/*.ts\"", + "format": "prettier --write \"src/**/*.ts\"", "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", @@ -19,25 +25,38 @@ "license": "ISC", "packageManager": "pnpm@10.28.1", "dependencies": { + "@socket.io/redis-adapter": "^8.3.0", "@stellar/stellar-sdk": "^15.1.0", "cors": "^2.8.6", "dotenv": "^17.3.1", "drizzle-orm": "^0.45.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", + "ioredis": "^5.11.0", "jsonwebtoken": "^9.0.3", "morgan": "^1.10.1", "postgres": "^3.4.9", - "socket.io": "^4.8.3" + "redis": "^6.0.0", + "socket.io": "^4.8.3", + "zod": "^4.4.3" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.10", "@types/node": "^20.19.37", + "@types/supertest": "^7.2.0", + "@vitest/coverage-v8": "^4.1.6", "drizzle-kit": "^0.31.10", + "eslint": "^9.39.4", + "prettier": "^3.8.3", + "supertest": "^7.2.2", "ts-node": "^10.9.2", "tsx": "^4.21.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "typescript-eslint": "^8.59.3", + "vitest": "^4.1.6" } } \ No newline at end of file diff --git a/apps/backend/src/__tests__/auth.integration.test.ts b/apps/backend/src/__tests__/auth.integration.test.ts new file mode 100644 index 0000000..ff542b6 --- /dev/null +++ b/apps/backend/src/__tests__/auth.integration.test.ts @@ -0,0 +1,247 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; + +// ── Mocks (must be declared before any imports that use them) ───────────── + +const mockCreateNonce = vi.fn(() => 'test-nonce-abc123'); +const mockConsumeNonce = vi.fn(); + +vi.mock('../lib/nonce.js', () => ({ + createNonce: mockCreateNonce, + consumeNonce: mockConsumeNonce, +})); + +const mockFindFirst = vi.fn(); +const mockInsert = vi.fn(); + +vi.mock('../db/index.js', () => ({ + db: { + query: { + wallets: { findFirst: mockFindFirst }, + }, + insert: mockInsert, + execute: vi.fn().mockResolvedValue([]), + }, +})); + +const mockVerify = vi.fn(() => true); +vi.mock('@stellar/stellar-sdk', () => ({ + Keypair: { + fromPublicKey: vi.fn(() => ({ verify: mockVerify })), + }, +})); + +// ── Import app after mocks are registered ───────────────────────────────── + +const { app } = await import('../app.js'); +const { challengeLimiter, verifyLimiter } = await import('../routes/auth.js'); + +function resetRateLimiters() { + challengeLimiter.resetKey('127.0.0.1'); + verifyLimiter.resetKey('127.0.0.1'); +} + +// ── Helpers ─────────────────────────────────────────────────────────────── + +const WALLET = 'GABCDEFGHIJKLMNOPQRSTUVWXYZ012345678901234567890123456789AB'; +const SIGNATURE = 'aabbccdd'; +const NONCE = 'test-nonce-abc123'; + +function setupInsert(userId = 'new-user-id') { + const returningFn = vi.fn().mockResolvedValue([{ id: userId }]); + const valuesFn = vi.fn().mockReturnValue({ returning: returningFn }); + mockInsert.mockReturnValue({ values: valuesFn }); + return { returningFn, valuesFn }; +} + +// ── Tests ───────────────────────────────────────────────────────────────── + +describe('POST /auth/challenge', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetRateLimiters(); + }); + + it('returns 200 with message and nonce for valid walletAddress', async () => { + const res = await request(app).post('/auth/challenge').send({ walletAddress: WALLET }); + + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('nonce', NONCE); + expect(res.body).toHaveProperty('message'); + expect(typeof res.body.message).toBe('string'); + expect(res.body.message).toContain(WALLET); + expect(mockCreateNonce).toHaveBeenCalledWith(WALLET); + }); + + it('returns 400 with error when walletAddress is missing', async () => { + const res = await request(app).post('/auth/challenge').send({}); + + expect(res.status).toBe(400); + expect(res.body).toHaveProperty('error'); + expect(mockCreateNonce).not.toHaveBeenCalled(); + }); + + it('returns 400 when body is completely absent', async () => { + const res = await request(app) + .post('/auth/challenge') + .set('Content-Type', 'application/json') + .send('{}'); + + expect(res.status).toBe(400); + }); +}); + +describe('POST /auth/verify', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetRateLimiters(); + }); + + it('returns 200 with JWT token for valid new-user flow', async () => { + mockConsumeNonce.mockReturnValue(true); + mockVerify.mockReturnValue(true); + mockFindFirst.mockResolvedValue(undefined); // no existing wallet → create user + setupInsert(); + + const res = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: NONCE }); + + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('token'); + const parts = (res.body.token as string).split('.'); + expect(parts).toHaveLength(3); // valid JWT structure + }); + + it('returns 200 with JWT for existing wallet (returning user)', async () => { + mockConsumeNonce.mockReturnValue(true); + mockVerify.mockReturnValue(true); + mockFindFirst.mockResolvedValue({ userId: 'existing-user-id', address: WALLET }); + + const res = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: NONCE }); + + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('token'); + }); + + it('returns 401 when nonce is expired or invalid', async () => { + mockConsumeNonce.mockReturnValue(false); + + const res = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: 'expired-nonce' }); + + expect(res.status).toBe(401); + expect(res.body).toHaveProperty('error'); + }); + + it('returns 401 when signature verification fails', async () => { + mockConsumeNonce.mockReturnValue(true); + mockVerify.mockReturnValue(false); + + const res = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: 'badsig', nonce: NONCE }); + + expect(res.status).toBe(401); + expect(res.body.error).toMatch(/signature/i); + }); + + it('returns 400 when required fields are missing', async () => { + const res = await request(app).post('/auth/verify').send({ walletAddress: WALLET }); + + expect(res.status).toBe(400); + expect(res.body).toHaveProperty('error'); + }); + + it('returns 400 when all fields are absent', async () => { + const res = await request(app).post('/auth/verify').send({}); + + expect(res.status).toBe(400); + expect(res.body).toHaveProperty('error'); + }); + + it('returns 401 when Stellar Keypair throws (malformed wallet address)', async () => { + mockConsumeNonce.mockReturnValue(true); + mockVerify.mockImplementation(() => { + throw new Error('invalid key'); + }); + + const res = await request(app) + .post('/auth/verify') + .send({ walletAddress: 'INVALID', signature: SIGNATURE, nonce: NONCE }); + + expect(res.status).toBe(401); + expect(res.body).toHaveProperty('error'); + }); +}); + +describe('Auth rate limiting', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetRateLimiters(); + mockConsumeNonce.mockReturnValue(true); + mockVerify.mockReturnValue(true); + mockFindFirst.mockResolvedValue({ userId: 'existing-user-id', address: WALLET }); + }); + + it('allows up to 10 /auth/challenge requests per minute, blocks the 11th with 429', async () => { + for (let i = 0; i < 10; i++) { + const res = await request(app).post('/auth/challenge').send({ walletAddress: WALLET }); + expect(res.status).toBe(200); + } + + const blocked = await request(app).post('/auth/challenge').send({ walletAddress: WALLET }); + expect(blocked.status).toBe(429); + expect(blocked.headers['retry-after']).toBeDefined(); + }); + + it('allows up to 5 /auth/verify requests per minute, blocks the 6th with 429', async () => { + for (let i = 0; i < 5; i++) { + const res = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: NONCE }); + expect(res.status).toBe(200); + } + + const blocked = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: NONCE }); + expect(blocked.status).toBe(429); + expect(blocked.headers['retry-after']).toBeDefined(); + }); + + it('challenge and verify limiters are independent', async () => { + // Exhaust verify limit + for (let i = 0; i < 5; i++) { + await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: NONCE }); + } + const verifyBlocked = await request(app) + .post('/auth/verify') + .send({ walletAddress: WALLET, signature: SIGNATURE, nonce: NONCE }); + expect(verifyBlocked.status).toBe(429); + + // Challenge limit should still allow requests + const challengeRes = await request(app).post('/auth/challenge').send({ walletAddress: WALLET }); + expect(challengeRes.status).toBe(200); + }); + + it('does not affect authenticated routes (/me returns its normal status under heavy load)', async () => { + // Hammer /me well past the auth limits — it must not return 429 + for (let i = 0; i < 20; i++) { + const res = await request(app).get('/me'); + expect(res.status).not.toBe(429); + } + }); + + it('does not affect the /health endpoint under heavy load', async () => { + for (let i = 0; i < 20; i++) { + const res = await request(app).get('/health'); + expect(res.status).not.toBe(429); + } + }); +}); diff --git a/apps/backend/src/__tests__/config.test.ts b/apps/backend/src/__tests__/config.test.ts new file mode 100644 index 0000000..cf4a381 --- /dev/null +++ b/apps/backend/src/__tests__/config.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { loadEnv, EnvSchema } from '../config.js'; + +const validEnv = { + DATABASE_URL: 'postgres://localhost/test', + REDIS_URL: 'redis://localhost:6379', + JWT_SECRET: 'test-secret', + PORT: '3001', + TOKEN_TRANSFER_CONTRACT_ID: 'CONTRACT123', +}; + +describe('loadEnv', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns parsed env and emits no output for a valid environment', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + const env = loadEnv({ ...validEnv }); + + expect(env).toEqual({ + DATABASE_URL: 'postgres://localhost/test', + REDIS_URL: 'redis://localhost:6379', + JWT_SECRET: 'test-secret', + PORT: 3001, + TOKEN_TRANSFER_CONTRACT_ID: 'CONTRACT123', + }); + expect(errorSpy).not.toHaveBeenCalled(); + expect(logSpy).not.toHaveBeenCalled(); + }); + + it('logs the missing variable and exits with code 1 when DATABASE_URL is absent', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as never); + + const { DATABASE_URL: _omitted, ...withoutDbUrl } = validEnv; + + const _ = _omitted; // eslint-disable-line @typescript-eslint/no-unused-vars + + expect(() => loadEnv(withoutDbUrl)).toThrow('process.exit called'); + expect(exitSpy).toHaveBeenCalledWith(1); + + const logged = errorSpy.mock.calls.map((args) => args.join(' ')).join('\n'); + expect(logged).toContain('DATABASE_URL'); + }); + + it('reports every missing variable on an empty environment', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as never); + + expect(() => loadEnv({})).toThrow('process.exit called'); + expect(exitSpy).toHaveBeenCalledWith(1); + + const logged = errorSpy.mock.calls.map((args) => args.join(' ')).join('\n'); + for (const key of Object.keys(validEnv)) { + expect(logged).toContain(key); + } + }); + + it('rejects a non-numeric PORT', () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as never); + + expect(() => loadEnv({ ...validEnv, PORT: 'not-a-number' })).toThrow('process.exit called'); + expect(exitSpy).toHaveBeenCalledWith(1); + expect(errorSpy.mock.calls.map((args) => args.join(' ')).join('\n')).toContain('PORT'); + }); + + it('coerces a numeric PORT string to a number', () => { + const parsed = EnvSchema.parse({ ...validEnv, PORT: '8080' }); + expect(parsed.PORT).toBe(8080); + }); +}); diff --git a/apps/backend/src/__tests__/conversations.cache.test.ts b/apps/backend/src/__tests__/conversations.cache.test.ts new file mode 100644 index 0000000..7e7f679 --- /dev/null +++ b/apps/backend/src/__tests__/conversations.cache.test.ts @@ -0,0 +1,290 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; + +// ── Redis mock ───────────────────────────────────────────────────────────── + +const mockGet = vi.fn(); +const mockSetex = vi.fn(); +const mockDel = vi.fn(); + +vi.mock('../lib/redis.js', () => ({ + get redis() { + return mockRedisInstance; + }, + CONV_CACHE_TTL: 30, + convCacheKey: (userId: string) => `conversations:${userId}`, +})); + +let mockRedisInstance: { + get: typeof mockGet; + setex: typeof mockSetex; + del: typeof mockDel; +} | null = { + get: mockGet, + setex: mockSetex, + del: mockDel, +}; + +// ── DB mock ──────────────────────────────────────────────────────────────── + +const mockFindMany = vi.fn(); +const mockFindFirst = vi.fn(); +const mockExecute = vi.fn(); +const mockGroupBy = vi.fn(); +const mockWhere = vi.fn(() => ({ groupBy: mockGroupBy })); +const mockFrom = vi.fn(() => ({ where: mockWhere })); +const mockSelect = vi.fn(() => ({ from: mockFrom })); + +vi.mock('../db/index.js', () => ({ + db: { + query: { + conversationMembers: { findMany: mockFindMany, findFirst: mockFindFirst }, + }, + execute: mockExecute, + select: mockSelect, + }, +})); + +vi.mock('../lib/socket.js', () => ({ + getSocketServer: () => null, +})); + +vi.mock('../db/schema.js', () => ({ + conversations: { id: 'id', type: 'type' }, + conversationMembers: { + conversationId: 'conversationId', + userId: 'userId', + joinedAt: 'joinedAt', + isArchived: 'isArchived', + }, + messages: { + id: 'id', + conversationId: 'conversationId', + senderId: 'senderId', + content: 'content', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }, + tokenTransfers: {}, +})); +vi.mock('drizzle-orm', () => { + const sqlMock = Object.assign( + vi.fn(() => 'sql'), + { + join: vi.fn(() => 'joined'), + }, + ); + + return { + and: vi.fn((...args: unknown[]) => args.filter(Boolean)), + asc: vi.fn(), + count: vi.fn(() => 'count'), + desc: vi.fn(), + eq: vi.fn((col: unknown, val: unknown) => ({ col, val })), + ne: vi.fn((col: unknown, val: unknown) => ({ col, val, op: 'ne' })), + lt: vi.fn(), + sql: sqlMock, + }; +}); + +// ── Auth middleware mock: always passes with test userId ─────────────────── + +const TEST_USER_ID = 'user-test-123'; + +vi.mock('../middleware/auth.js', () => ({ + requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => { + (req as express.Request & { auth: { userId: string } }).auth = { userId: TEST_USER_ID }; + next(); + }, +})); + +// ── Import router after mocks ────────────────────────────────────────────── + +const { conversationsRouter } = await import('../routes/conversations.js'); + +function makeApp() { + const app = express(); + app.use(express.json()); + app.use('/conversations', conversationsRouter); + return app; +} + +// ── Tests ────────────────────────────────────────────────────────────────── + +describe('GET /conversations — Redis caching', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRedisInstance = { get: mockGet, setex: mockSetex, del: mockDel }; + mockGroupBy.mockResolvedValue([]); + mockExecute.mockResolvedValue([]); + }); + + it('returns cached data without hitting DB on cache hit', async () => { + const cached = [{ id: 'conv-1', type: 'dm' }]; + mockGet.mockResolvedValue(JSON.stringify(cached)); + + const res = await request(makeApp()).get('/conversations'); + + expect(res.status).toBe(200); + expect(res.body).toEqual(cached); + expect(mockFindMany).not.toHaveBeenCalled(); + }); + + it('queries DB and writes to cache on cache miss', async () => { + mockGet.mockResolvedValue(null); // cache miss + mockFindMany.mockResolvedValue([ + { conversationId: 'conv-2', conversation: { id: 'conv-2', type: 'group', messages: [] } }, + ]); + mockSetex.mockResolvedValue('OK'); + + const res = await request(makeApp()).get('/conversations'); + + expect(res.status).toBe(200); + expect(mockFindMany).toHaveBeenCalled(); + expect(mockSetex).toHaveBeenCalledWith(`conversations:${TEST_USER_ID}`, 30, expect.any(String)); + }); + + it('falls back to DB when Redis is unavailable (redis is null)', async () => { + mockRedisInstance = null; // simulate no Redis + const dbResult = [{ id: 'conv-3' }]; + mockFindMany.mockResolvedValue( + dbResult.map((c) => ({ conversationId: c.id, conversation: c })), + ); + + const res = await request(makeApp()).get('/conversations'); + + expect(res.status).toBe(200); + expect(mockFindMany).toHaveBeenCalled(); + expect(mockGet).not.toHaveBeenCalled(); + }); + + it('falls back to DB when Redis.get throws', async () => { + mockGet.mockRejectedValue(new Error('Redis connection refused')); + const dbResult = [{ id: 'conv-4' }]; + mockFindMany.mockResolvedValue( + dbResult.map((c) => ({ conversationId: c.id, conversation: c })), + ); + mockSetex.mockResolvedValue('OK'); + + const res = await request(makeApp()).get('/conversations'); + + expect(res.status).toBe(200); + expect(mockFindMany).toHaveBeenCalled(); + }); + + it('uses per-user cache key (conversations:)', async () => { + mockGet.mockResolvedValue(null); + mockFindMany.mockResolvedValue([]); + mockSetex.mockResolvedValue('OK'); + + await request(makeApp()).get('/conversations'); + + expect(mockGet).toHaveBeenCalledWith(`conversations:${TEST_USER_ID}`); + expect(mockSetex).toHaveBeenCalledWith( + `conversations:${TEST_USER_ID}`, + expect.any(Number), + expect.any(String), + ); + }); +}); + +describe('GET /conversations/:id/search', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRedisInstance = { get: mockGet, setex: mockSetex, del: mockDel }; + }); + + it('returns 400 when the query is empty', async () => { + const res = await request(makeApp()).get('/conversations/conv-1/search?q= '); + + expect(res.status).toBe(400); + expect(mockFindFirst).not.toHaveBeenCalled(); + expect(mockExecute).not.toHaveBeenCalled(); + }); + + it('returns 403 when the user is not a conversation member', async () => { + mockFindFirst.mockResolvedValue(undefined); + + const res = await request(makeApp()).get('/conversations/conv-1/search?q=hello'); + + expect(res.status).toBe(403); + expect(mockExecute).not.toHaveBeenCalled(); + }); + + it('returns ranked highlighted matches for conversation members', async () => { + const searchResults = [ + { + id: 'msg-1', + conversationId: 'conv-1', + senderId: TEST_USER_ID, + content: 'hello from stellar', + snippet: 'hello from stellar', + rank: '0.1', + }, + ]; + mockFindFirst.mockResolvedValue({ id: 'member-1' }); + mockExecute.mockResolvedValue(searchResults); + + const res = await request(makeApp()).get('/conversations/conv-1/search?q=hello'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ results: searchResults }); + expect(mockExecute).toHaveBeenCalledTimes(1); + }); +}); + +describe('GET /conversations — isArchived filter', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRedisInstance = null; // bypass Redis for these tests + mockGroupBy.mockResolvedValue([]); + mockExecute.mockResolvedValue([]); + }); + + it('excludes archived conversations by default (no ?archived param)', async () => { + const { ne } = await import('drizzle-orm'); + mockFindMany.mockResolvedValue([]); + + const res = await request(makeApp()).get('/conversations'); + + expect(res.status).toBe(200); + // ne(isArchived, true) must appear in the where clause + expect(ne).toHaveBeenCalledWith( + expect.anything(), // conversationMembers.isArchived column + true, + ); + }); + + it('excludes archived conversations when ?archived=false', async () => { + const { ne } = await import('drizzle-orm'); + mockFindMany.mockResolvedValue([]); + + const res = await request(makeApp()).get('/conversations?archived=false'); + + expect(res.status).toBe(200); + expect(ne).toHaveBeenCalledWith(expect.anything(), true); + }); + + it('includes archived conversations when ?archived=true', async () => { + const { ne } = await import('drizzle-orm'); + mockFindMany.mockResolvedValue([]); + + const res = await request(makeApp()).get('/conversations?archived=true'); + + expect(res.status).toBe(200); + // ne should NOT be called — all conversations returned regardless of archived state + expect(ne).not.toHaveBeenCalled(); + }); + + it('skips cache read and write when ?archived=true', async () => { + mockRedisInstance = { get: mockGet, setex: mockSetex, del: mockDel }; + mockFindMany.mockResolvedValue([]); + + const res = await request(makeApp()).get('/conversations?archived=true'); + + expect(res.status).toBe(200); + expect(mockGet).not.toHaveBeenCalled(); + expect(mockSetex).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/backend/src/__tests__/conversations.routes.test.ts b/apps/backend/src/__tests__/conversations.routes.test.ts new file mode 100644 index 0000000..f402138 --- /dev/null +++ b/apps/backend/src/__tests__/conversations.routes.test.ts @@ -0,0 +1,442 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; + +const conversationsTable = { id: 'id', type: 'type' }; +const conversationMembersTable = { + conversationId: 'conversationId', + userId: 'userId', + joinedAt: 'joinedAt', +}; + +const mockFindConversation = vi.fn(); +const mockFindMember = vi.fn(); +const mockFindMany = vi.fn(); +const mockDelete = vi.fn(); +const mockReturning = vi.fn(); +const mockValues = vi.fn(() => ({ returning: mockReturning })); +const mockInsert = vi.fn(() => ({ values: mockValues })); +const mockEmit = vi.fn(); +const mockTo = vi.fn(() => ({ emit: mockEmit })); +const mockUpdateReturning = vi.fn(); +const mockUpdateWhere = vi.fn(() => ({ returning: mockUpdateReturning })); +const mockUpdateSet = vi.fn(() => ({ where: mockUpdateWhere })); +const mockUpdate = vi.fn(() => ({ set: mockUpdateSet })); + +vi.mock('../lib/socket.js', () => ({ + getSocketServer: () => ({ to: mockTo }), +})); + +vi.mock('../lib/redis.js', () => ({ + get redis() { + return null; + }, + CONV_CACHE_TTL: 30, + convCacheKey: (userId: string) => `conversations:${userId}`, +})); + +vi.mock('../db/index.js', () => ({ + db: { + query: { + conversations: { findFirst: mockFindConversation }, + conversationMembers: { findFirst: mockFindMember, findMany: mockFindMany }, + }, + delete: mockDelete, + insert: mockInsert, + update: mockUpdate, + }, +})); + +vi.mock('../db/schema.js', () => ({ + conversations: conversationsTable, + conversationMembers: conversationMembersTable, + messages: { + id: 'id', + conversationId: 'conversationId', + senderId: 'senderId', + content: 'content', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }, + tokenTransfers: {}, +})); + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...args: unknown[]) => args.filter(Boolean)), + asc: vi.fn(), + eq: vi.fn((col: unknown, val: unknown) => ({ col, val })), + ne: vi.fn((col: unknown, val: unknown) => ({ col, val, op: 'ne' })), + desc: vi.fn(), + lt: vi.fn(), + sql: vi.fn(), +})); + +vi.mock('../middleware/auth.js', () => ({ + requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => { + (req as express.Request & { auth: { userId: string } }).auth = { userId: 'user-1' }; + next(); + }, +})); + +const { conversationsRouter } = await import('../routes/conversations.js'); + +function makeApp() { + const app = express(); + app.use(express.json()); + app.use('/conversations', conversationsRouter); + return app; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('GET /conversations/:id', () => { + it('returns 404 for an unknown conversation', async () => { + mockFindConversation.mockResolvedValue(undefined); + + const res = await request(makeApp()).get('/conversations/conv-1'); + + expect(res.status).toBe(404); + expect(mockFindMember).not.toHaveBeenCalled(); + }); + + it('returns 403 when the caller is not a member', async () => { + mockFindConversation.mockResolvedValue({ + id: 'conv-1', + type: 'group', + members: [], + messages: [], + }); + mockFindMember.mockResolvedValue(undefined); + + const res = await request(makeApp()).get('/conversations/conv-1'); + + expect(res.status).toBe(403); + }); + + it('returns the same conversation shape as the list endpoint', async () => { + const conversation = { + id: 'conv-1', + type: 'group', + name: 'General', + members: [ + { + id: 'member-1', + conversationId: 'conv-1', + userId: 'user-1', + user: { + id: 'user-1', + username: 'alice', + avatarUrl: null, + wallets: [], + }, + }, + ], + messages: [ + { + id: 'msg-1', + conversationId: 'conv-1', + senderId: 'user-1', + content: 'hello', + deletedAt: null, + sender: { + id: 'user-1', + username: 'alice', + avatarUrl: null, + }, + }, + ], + }; + + mockFindConversation.mockResolvedValue(conversation); + mockFindMember.mockResolvedValue({ id: 'member-1' }); + + const res = await request(makeApp()).get('/conversations/conv-1'); + + expect(res.status).toBe(200); + expect(res.body.id).toBe('conv-1'); + expect(res.body.messages).toHaveLength(1); + expect(res.body.messages[0].content).toBe('hello'); + }); +}); + +describe('GET /conversations/:id/members', () => { + it('returns 403 when the caller is not a member', async () => { + mockFindMember.mockResolvedValue(undefined); + + const res = await request(makeApp()).get('/conversations/conv-1/members'); + + expect(res.status).toBe(403); + expect(mockFindMany).not.toHaveBeenCalled(); + }); + + it('returns conversation members with primary wallet addresses and joinedAt', async () => { + const joinedAt = new Date('2026-05-31T10:00:00.000Z'); + + mockFindMember.mockResolvedValue({ id: 'member-1' }); + mockFindMany.mockResolvedValue([ + { + joinedAt, + user: { + id: 'user-1', + username: 'alice', + avatarUrl: null, + wallets: [ + { address: 'GSECONDARY', isPrimary: false }, + { address: 'GPRIMARY', isPrimary: true }, + ], + }, + }, + { + joinedAt, + user: { + id: 'user-2', + username: 'bob', + avatarUrl: 'https://example.com/bob.png', + wallets: [], + }, + }, + ]); + + const res = await request(makeApp()).get('/conversations/conv-1/members'); + + expect(res.status).toBe(200); + expect(res.body.members).toEqual([ + { + id: 'user-1', + username: 'alice', + avatarUrl: null, + primaryWalletAddress: 'GPRIMARY', + joinedAt: joinedAt.toISOString(), + }, + { + id: 'user-2', + username: 'bob', + avatarUrl: 'https://example.com/bob.png', + primaryWalletAddress: null, + joinedAt: joinedAt.toISOString(), + }, + ]); + }); +}); + +describe('POST /conversations/:id/members', () => { + it('returns 400 for DM conversations', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-dm', type: 'dm' }); + + const res = await request(makeApp()) + .post('/conversations/conv-dm/members') + .send({ userId: 'user-2' }); + + expect(res.status).toBe(400); + expect(mockInsert).not.toHaveBeenCalled(); + }); + + it('returns 403 when the caller is not a member', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue(undefined); + + const res = await request(makeApp()) + .post('/conversations/conv-1/members') + .send({ userId: 'user-2' }); + + expect(res.status).toBe(403); + expect(mockInsert).not.toHaveBeenCalled(); + }); + + it('returns 409 when the user is already a member', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember + .mockResolvedValueOnce({ id: 'member-1' }) + .mockResolvedValueOnce({ id: 'member-2' }); + + const res = await request(makeApp()) + .post('/conversations/conv-1/members') + .send({ userId: 'user-2' }); + + expect(res.status).toBe(409); + expect(mockInsert).not.toHaveBeenCalled(); + }); + + it('adds a member to a group conversation and broadcasts member_joined', async () => { + const joinedAt = new Date('2026-05-31T11:00:00.000Z'); + + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValueOnce({ id: 'member-1' }).mockResolvedValueOnce(undefined); + mockReturning.mockResolvedValue([ + { + id: 'member-2', + conversationId: 'conv-1', + userId: 'user-2', + joinedAt, + }, + ]); + mockFindMany.mockResolvedValue([{ userId: 'user-1' }, { userId: 'user-2' }]); + + const res = await request(makeApp()) + .post('/conversations/conv-1/members') + .send({ userId: 'user-2' }); + + expect(res.status).toBe(201); + expect(mockInsert).toHaveBeenCalledWith(conversationMembersTable); + expect(mockValues).toHaveBeenCalledWith({ conversationId: 'conv-1', userId: 'user-2' }); + expect(mockTo).toHaveBeenCalledWith('conv-1'); + expect(mockEmit).toHaveBeenCalledWith('member_joined', { + userId: 'user-2', + conversationId: 'conv-1', + }); + expect(res.body).toEqual({ + id: 'member-2', + conversationId: 'conv-1', + userId: 'user-2', + joinedAt: joinedAt.toISOString(), + }); + }); +}); + +describe('PATCH /conversations/:id', () => { + it('returns 400 for DM conversations', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-dm', type: 'dm' }); + + const res = await request(makeApp()).patch('/conversations/conv-dm').send({ name: 'New Name' }); + + expect(res.status).toBe(400); + expect(mockUpdateSet).not.toHaveBeenCalled(); + }); + + it('returns 403 when the caller is not a member', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue(undefined); + + const res = await request(makeApp()).patch('/conversations/conv-1').send({ name: 'New Name' }); + + expect(res.status).toBe(403); + expect(mockUpdateSet).not.toHaveBeenCalled(); + }); + + it('returns 400 when neither name nor avatarUrl is provided', async () => { + const res = await request(makeApp()).patch('/conversations/conv-1').send({}); + + expect(res.status).toBe(400); + }); + + it('updates the conversation name and broadcasts conversation_updated', async () => { + const updatedConv = { + id: 'conv-1', + type: 'group', + name: 'New Name', + avatarUrl: null, + createdAt: new Date('2026-05-31T10:00:00.000Z'), + }; + + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue({ id: 'member-1' }); + mockUpdateReturning.mockResolvedValue([updatedConv]); + mockFindMany.mockResolvedValue([{ userId: 'user-1' }, { userId: 'user-2' }]); + + const res = await request(makeApp()).patch('/conversations/conv-1').send({ name: 'New Name' }); + + expect(res.status).toBe(200); + expect(mockUpdate).toHaveBeenCalled(); + expect(mockUpdateSet).toHaveBeenCalled(); + expect(mockUpdateWhere).toHaveBeenCalled(); + expect(mockTo).toHaveBeenCalledWith('conv-1'); + expect(mockEmit).toHaveBeenCalledWith('conversation_updated', { + id: updatedConv.id, + type: updatedConv.type, + name: updatedConv.name, + avatarUrl: updatedConv.avatarUrl, + createdAt: updatedConv.createdAt, + }); + expect(res.body).toEqual({ + id: updatedConv.id, + type: updatedConv.type, + name: updatedConv.name, + avatarUrl: updatedConv.avatarUrl, + createdAt: updatedConv.createdAt.toISOString(), + }); + }); + + it('updates the conversation avatarUrl and broadcasts conversation_updated', async () => { + const updatedConv = { + id: 'conv-1', + type: 'group', + name: 'General', + avatarUrl: 'https://example.com/avatar.png', + createdAt: new Date('2026-05-31T10:00:00.000Z'), + }; + + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue({ id: 'member-1' }); + mockUpdateReturning.mockResolvedValue([updatedConv]); + mockFindMany.mockResolvedValue([{ userId: 'user-1' }, { userId: 'user-2' }]); + + const res = await request(makeApp()) + .patch('/conversations/conv-1') + .send({ avatarUrl: 'https://example.com/avatar.png' }); + + expect(res.status).toBe(200); + expect(mockTo).toHaveBeenCalledWith('conv-1'); + expect(mockEmit).toHaveBeenCalledWith('conversation_updated', { + id: updatedConv.id, + type: updatedConv.type, + name: updatedConv.name, + avatarUrl: updatedConv.avatarUrl, + createdAt: updatedConv.createdAt, + }); + expect(res.body).toEqual({ + id: updatedConv.id, + type: updatedConv.type, + name: updatedConv.name, + avatarUrl: updatedConv.avatarUrl, + createdAt: updatedConv.createdAt.toISOString(), + }); + }); +}); + +describe('DELETE /conversations/:id/leave', () => { + it('returns 400 for DM conversations', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-dm', type: 'dm' }); + + const res = await request(makeApp()).delete('/conversations/conv-dm/leave'); + + expect(res.status).toBe(400); + }); + + it('returns 404 when the caller is not a member', async () => { + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue(undefined); + + const res = await request(makeApp()).delete('/conversations/conv-1/leave'); + + expect(res.status).toBe(404); + }); + + it('deletes the conversation when the last member leaves', async () => { + const deleteWhere = vi.fn().mockResolvedValue(undefined); + mockDelete.mockReturnValue({ where: deleteWhere }); + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue({ id: 'member-1' }); + mockFindMany.mockResolvedValue([{ userId: 'user-1' }]); + + const res = await request(makeApp()).delete('/conversations/conv-1/leave'); + + expect(res.status).toBe(204); + expect(mockDelete).toHaveBeenCalledWith(conversationsTable); + }); + + it('removes only the caller when other members remain', async () => { + const deleteWhere = vi.fn().mockResolvedValue(undefined); + mockDelete.mockReturnValue({ where: deleteWhere }); + mockFindConversation.mockResolvedValue({ id: 'conv-1', type: 'group' }); + mockFindMember.mockResolvedValue({ id: 'member-1' }); + mockFindMany.mockResolvedValue([{ userId: 'user-1' }, { userId: 'user-2' }]); + + const res = await request(makeApp()).delete('/conversations/conv-1/leave'); + + expect(res.status).toBe(204); + expect(mockDelete).toHaveBeenCalledWith(conversationMembersTable); + expect(deleteWhere).toHaveBeenCalled(); + }); +}); diff --git a/apps/backend/src/__tests__/health.test.ts b/apps/backend/src/__tests__/health.test.ts new file mode 100644 index 0000000..1371028 --- /dev/null +++ b/apps/backend/src/__tests__/health.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; + +const mockExecute = vi.fn(); + +vi.mock('../db/index.js', () => ({ + db: { + execute: mockExecute, + query: { + conversations: { findFirst: vi.fn() }, + conversationMembers: { findFirst: vi.fn(), findMany: vi.fn() }, + messages: { findFirst: vi.fn() }, + tokenTransfers: { findFirst: vi.fn(), findMany: vi.fn() }, + users: { findFirst: vi.fn() }, + wallets: { findFirst: vi.fn() }, + }, + }, +})); + +const { app } = await import('../app.js'); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('GET /health', () => { + it('returns the db status, node version, and app version', async () => { + mockExecute.mockResolvedValue([]); + + const res = await request(app).get('/health'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + status: 'ok', + db: 'connected', + node: process.version, + version: '1.0.0', + }); + }); + + it('returns 503 with the same version fields when the db is unreachable', async () => { + mockExecute.mockRejectedValue(new Error('db down')); + + const res = await request(app).get('/health'); + + expect(res.status).toBe(503); + expect(res.body).toEqual({ + status: 'error', + db: 'unreachable', + node: process.version, + version: '1.0.0', + }); + }); +}); diff --git a/apps/backend/src/__tests__/jwt.test.ts b/apps/backend/src/__tests__/jwt.test.ts new file mode 100644 index 0000000..524a45d --- /dev/null +++ b/apps/backend/src/__tests__/jwt.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { signToken, verifyToken } from '../lib/jwt.js'; + +describe('JWT utilities', () => { + const payload = { userId: 'user-123', walletAddress: 'GABCDE' }; + + it('signs a token without throwing', () => { + const token = signToken(payload); + expect(typeof token).toBe('string'); + expect(token.split('.')).toHaveLength(3); + }); + + it('verifies a valid token and returns the payload', () => { + const token = signToken(payload); + const decoded = verifyToken(token); + expect(decoded.userId).toBe(payload.userId); + expect(decoded.walletAddress).toBe(payload.walletAddress); + }); + + it('throws on a tampered token', () => { + const token = signToken(payload); + const tampered = token.slice(0, -4) + 'xxxx'; + expect(() => verifyToken(tampered)).toThrow(); + }); + + it('throws on an expired token', async () => { + const jwt = await import('jsonwebtoken'); + const secret = process.env['JWT_SECRET']!; + const expired = jwt.default.sign(payload, secret, { expiresIn: -1 }); + expect(() => verifyToken(expired)).toThrow(/expired/i); + }); +}); diff --git a/apps/backend/src/__tests__/keyBundle.test.ts b/apps/backend/src/__tests__/keyBundle.test.ts new file mode 100644 index 0000000..ecf253d --- /dev/null +++ b/apps/backend/src/__tests__/keyBundle.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockFindDevice = vi.fn(); +const mockExecute = vi.fn(); + +vi.mock('../db/index.js', () => ({ + db: { + query: { devices: { findFirst: mockFindDevice } }, + execute: mockExecute, + }, +})); + +vi.mock('../db/schema.js', () => ({ + devices: { id: 'id', userId: 'userId' }, + oneTimePreKeys: { deviceId: 'deviceId', keyId: 'keyId', consumed: 'consumed' }, +})); + +vi.mock('drizzle-orm', () => ({ + and: (...args: unknown[]) => args.filter(Boolean), + eq: (col: unknown, val: unknown) => ({ col, val }), + sql: vi.fn(), +})); + +const { fetchAndConsumeKeyBundle } = await import('../services/keyBundle.js'); + +const DEVICE = { + id: 'dev-1', + userId: 'user-1', + identityPublicKey: 'identity-pub', + registrationId: 4242, + signedPreKeyId: 7, + signedPreKeyPublic: 'spk-pub', + signedPreKeySignature: 'spk-sig', + revokedAt: null, + createdAt: new Date('2026-01-01'), +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('fetchAndConsumeKeyBundle', () => { + it('returns 404 for an unknown device and does not touch prekeys', async () => { + mockFindDevice.mockResolvedValue(undefined); + + const result = await fetchAndConsumeKeyBundle('user-1', 'dev-1'); + + expect(result).toEqual({ ok: false, status: 404, error: 'Device not found' }); + expect(mockExecute).not.toHaveBeenCalled(); + }); + + it('returns 404 for a revoked device and does not consume a prekey', async () => { + mockFindDevice.mockResolvedValue({ ...DEVICE, revokedAt: new Date('2026-06-01') }); + + const result = await fetchAndConsumeKeyBundle('user-1', 'dev-1'); + + expect(result).toMatchObject({ ok: false, status: 404 }); + expect(mockExecute).not.toHaveBeenCalled(); + }); + + it('returns the bundle and consumes one one-time prekey', async () => { + mockFindDevice.mockResolvedValue(DEVICE); + mockExecute.mockResolvedValue([{ keyId: 100, publicKey: 'otp-pub' }]); + + const result = await fetchAndConsumeKeyBundle('user-1', 'dev-1'); + + expect(result).toEqual({ + ok: true, + bundle: { + identityPublicKey: 'identity-pub', + registrationId: 4242, + signedPreKey: { keyId: 7, publicKey: 'spk-pub', signature: 'spk-sig' }, + oneTimePreKey: { keyId: 100, publicKey: 'otp-pub' }, + }, + }); + expect(mockExecute).toHaveBeenCalledTimes(1); + }); + + it('returns oneTimePreKey: null when the pool is exhausted', async () => { + mockFindDevice.mockResolvedValue(DEVICE); + mockExecute.mockResolvedValue([]); + + const result = await fetchAndConsumeKeyBundle('user-1', 'dev-1'); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.bundle.oneTimePreKey).toBeNull(); + // The signed prekey is still served so a session can be established. + expect(result.bundle.signedPreKey.keyId).toBe(7); + } + }); + + it('never exposes private key material', async () => { + mockFindDevice.mockResolvedValue(DEVICE); + mockExecute.mockResolvedValue([{ keyId: 100, publicKey: 'otp-pub' }]); + + const result = await fetchAndConsumeKeyBundle('user-1', 'dev-1'); + + if (!result.ok) throw new Error('expected a bundle'); + const serialized = JSON.stringify(result.bundle).toLowerCase(); + expect(serialized).not.toContain('private'); + expect(serialized).not.toContain('secret'); + expect(Object.keys(result.bundle).sort()).toEqual([ + 'identityPublicKey', + 'oneTimePreKey', + 'registrationId', + 'signedPreKey', + ]); + }); + + it('hands out distinct prekeys to concurrent fetches, then null', async () => { + // Emulate the DB-side atomic claim: each UPDATE pops one key from the pool. + const pool = [ + { keyId: 1, publicKey: 'a' }, + { keyId: 2, publicKey: 'b' }, + ]; + mockFindDevice.mockResolvedValue(DEVICE); + mockExecute.mockImplementation(() => { + const claimed = pool.shift(); + return Promise.resolve(claimed ? [claimed] : []); + }); + + const [first, second, third] = await Promise.all([ + fetchAndConsumeKeyBundle('user-1', 'dev-1'), + fetchAndConsumeKeyBundle('user-1', 'dev-1'), + fetchAndConsumeKeyBundle('user-1', 'dev-1'), + ]); + + const otps = [first, second, third].map((r) => (r.ok ? r.bundle.oneTimePreKey : undefined)); + const issued = otps.filter((o): o is { keyId: number; publicKey: string } => o != null); + + expect(issued).toHaveLength(2); + expect(new Set(issued.map((o) => o.keyId)).size).toBe(2); // no key handed out twice + expect(otps.filter((o) => o === null)).toHaveLength(1); // exhausted fetch gets null + }); +}); diff --git a/apps/backend/src/__tests__/messages.routes.test.ts b/apps/backend/src/__tests__/messages.routes.test.ts new file mode 100644 index 0000000..f48ae04 --- /dev/null +++ b/apps/backend/src/__tests__/messages.routes.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; + +const mockFindMessage = vi.fn(); +const mockFindMembers = vi.fn(); +const mockUpdate = vi.fn(); + +const mockEmit = vi.fn(); +const mockTo = vi.fn(() => ({ emit: mockEmit })); +let mockSocketServer: { to: typeof mockTo } | null = { to: mockTo }; + +vi.mock('../lib/socket.js', () => ({ + getSocketServer() { + return mockSocketServer; + }, +})); + +vi.mock('../lib/redis.js', () => ({ + get redis() { + return null; + }, + CONV_CACHE_TTL: 30, + convCacheKey: (userId: string) => `conversations:${userId}`, +})); + +vi.mock('../db/index.js', () => ({ + db: { + query: { + messages: { findFirst: mockFindMessage }, + conversationMembers: { findMany: mockFindMembers }, + }, + update: mockUpdate, + }, +})); + +vi.mock('../db/schema.js', () => ({ + conversations: {}, + conversationMembers: { conversationId: 'conversationId', userId: 'userId' }, + messages: { + id: 'id', + conversationId: 'conversationId', + senderId: 'senderId', + content: 'content', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }, + tokenTransfers: {}, +})); + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...args: unknown[]) => args), + eq: vi.fn((col: unknown, val: unknown) => ({ col, val })), + desc: vi.fn(), + lt: vi.fn(), + sql: vi.fn(), +})); + +vi.mock('../middleware/auth.js', () => ({ + requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => { + (req as express.Request & { auth: { userId: string } }).auth = { userId: 'user-1' }; + next(); + }, +})); + +const { messagesRouter } = await import('../routes/messages.js'); + +function makeApp() { + const app = express(); + app.use(express.json()); + app.use('/messages', messagesRouter); + return app; +} + +beforeEach(() => { + vi.clearAllMocks(); + mockSocketServer = { to: mockTo }; +}); + +describe('DELETE /messages/:id', () => { + it('returns 403 when the caller is not the sender', async () => { + mockFindMessage.mockResolvedValue({ + id: 'msg-1', + conversationId: 'conv-1', + senderId: 'user-2', + content: 'hello', + deletedAt: null, + }); + + const res = await request(makeApp()).delete('/messages/msg-1'); + + expect(res.status).toBe(403); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('soft-deletes the caller message and broadcasts message_deleted', async () => { + mockFindMessage.mockResolvedValue({ + id: 'msg-1', + conversationId: 'conv-1', + senderId: 'user-1', + content: 'hello', + deletedAt: null, + }); + + const setFn = vi.fn().mockReturnThis(); + const whereFn = vi.fn().mockResolvedValue([{ conversationId: 'conv-1' }]); + mockUpdate.mockReturnValue({ set: setFn }); + setFn.mockReturnValue({ where: whereFn }); + mockFindMembers.mockResolvedValue([{ userId: 'user-1' }, { userId: 'user-2' }]); + + const res = await request(makeApp()).delete('/messages/msg-1'); + + expect(res.status).toBe(204); + expect(setFn).toHaveBeenCalledWith({ deletedAt: expect.any(Date) }); + expect(mockTo).toHaveBeenCalledWith('conv-1'); + expect(mockEmit).toHaveBeenCalledWith('message_deleted', { + messageId: 'msg-1', + conversationId: 'conv-1', + }); + }); +}); diff --git a/apps/backend/src/__tests__/nonce.test.ts b/apps/backend/src/__tests__/nonce.test.ts new file mode 100644 index 0000000..799ba11 --- /dev/null +++ b/apps/backend/src/__tests__/nonce.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { createNonce, consumeNonce } from '../lib/nonce.js'; + +describe('Nonce store', () => { + const wallet = 'GABCDEFGHIJKLMNOP'; + + it('creates a 32-char hex nonce', () => { + const nonce = createNonce(wallet); + expect(nonce).toMatch(/^[0-9a-f]{32}$/); + }); + + it('consuming a valid nonce returns true', () => { + const nonce = createNonce(wallet); + expect(consumeNonce(wallet, nonce)).toBe(true); + }); + + it('consuming the same nonce twice returns false (single-use)', () => { + const nonce = createNonce(wallet); + consumeNonce(wallet, nonce); + expect(consumeNonce(wallet, nonce)).toBe(false); + }); + + it('consuming a wrong nonce returns false', () => { + createNonce(wallet); + expect(consumeNonce(wallet, 'wrong-nonce')).toBe(false); + }); + + it('consuming a nonce for an unknown wallet returns false', () => { + expect(consumeNonce('UNKNOWN_WALLET', 'any-nonce')).toBe(false); + }); + + describe('expiry', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + it('rejects a nonce after 5 minutes have passed', () => { + const nonce = createNonce(wallet); + // Advance time past the 5-minute TTL + vi.advanceTimersByTime(5 * 60 * 1000 + 1); + expect(consumeNonce(wallet, nonce)).toBe(false); + }); + + it('accepts a nonce just before expiry', () => { + const nonce = createNonce(wallet); + vi.advanceTimersByTime(5 * 60 * 1000 - 1); + expect(consumeNonce(wallet, nonce)).toBe(true); + }); + }); +}); diff --git a/apps/backend/src/__tests__/readReceipts.test.ts b/apps/backend/src/__tests__/readReceipts.test.ts new file mode 100644 index 0000000..c70280c --- /dev/null +++ b/apps/backend/src/__tests__/readReceipts.test.ts @@ -0,0 +1,182 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { EventEmitter } from 'events'; + +// ── Mock DB ──────────────────────────────────────────────────────────────── + +const mockFindFirst = vi.fn(); +const mockUpdate = vi.fn(); + +vi.mock('../db/index.js', () => ({ + db: { + query: { + conversationMembers: { findFirst: mockFindFirst }, + messages: { findFirst: mockFindFirst }, + }, + update: mockUpdate, + }, +})); + +vi.mock('../db/schema.js', () => ({ + conversationMembers: {}, + conversations: {}, + messages: {}, +})); + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...args: unknown[]) => args), + eq: vi.fn((col: unknown, val: unknown) => ({ col, val })), + lt: vi.fn(), + desc: vi.fn(), +})); + +// ── Mock Socket helpers ──────────────────────────────────────────────────── + +function makeSocket(userId: string) { + const emitter = new EventEmitter(); + const emitted: { event: string; data: unknown }[] = []; + + const socket = Object.assign(emitter, { + auth: { userId }, + emit: vi.fn((event: string, data: unknown) => { + emitted.push({ event, data }); + }), + join: vi.fn(), + emitted, + }); + + return socket; +} + +function makeIo() { + const roomEmitted: { event: string; data: unknown }[] = []; + const io = { + to: vi.fn(() => ({ + emit: vi.fn((event: string, data: unknown) => { + roomEmitted.push({ event, data }); + }), + })), + roomEmitted, + }; + return io; +} + +// ── Tests ────────────────────────────────────────────────────────────────── + +describe('message_read socket event', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('persists last_read_message_id and broadcasts read_receipt', async () => { + const userId = 'user-abc'; + const conversationId = 'conv-1'; + const lastReadMessageId = 'msg-99'; + + // findFirst called twice: membership check, then message check + mockFindFirst + .mockResolvedValueOnce({ id: 'membership-1', userId, conversationId }) // membership + .mockResolvedValueOnce({ id: lastReadMessageId, conversationId }); // message + + const setFn = vi.fn().mockReturnThis(); + const whereFn = vi.fn().mockResolvedValue(undefined); + mockUpdate.mockReturnValue({ set: setFn }); + setFn.mockReturnValue({ where: whereFn }); + + const socket = makeSocket(userId); + const io = makeIo(); + + const { registerMessagingHandlers } = await import('../socket/messaging.js'); + registerMessagingHandlers(io as never, socket as never); + + const handler = (socket as EventEmitter).listeners('message_read')[0] as ( + p: unknown, + ) => Promise; + await handler({ conversationId, lastReadMessageId }); + + expect(mockUpdate).toHaveBeenCalled(); + expect(setFn).toHaveBeenCalledWith({ lastReadMessageId }); + expect(io.to).toHaveBeenCalledWith(conversationId); + }); + + it('emits error when caller is not a conversation member', async () => { + const socket = makeSocket('outsider'); + const io = makeIo(); + + mockFindFirst.mockResolvedValueOnce(undefined); // no membership + + const { registerMessagingHandlers } = await import('../socket/messaging.js'); + registerMessagingHandlers(io as never, socket as never); + + const handler = (socket as EventEmitter).listeners('message_read')[0] as ( + p: unknown, + ) => Promise; + await handler({ conversationId: 'conv-x', lastReadMessageId: 'msg-1' }); + + expect(socket.emit).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ + event: 'message_read', + message: expect.stringContaining('member'), + }), + ); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('emits error when message does not belong to the conversation', async () => { + const userId = 'user-abc'; + mockFindFirst + .mockResolvedValueOnce({ id: 'm1', userId, conversationId: 'conv-1' }) // membership ok + .mockResolvedValueOnce(undefined); // message not found + + const setFn = vi.fn().mockReturnThis(); + mockUpdate.mockReturnValue({ set: setFn }); + + const socket = makeSocket(userId); + const io = makeIo(); + + const { registerMessagingHandlers } = await import('../socket/messaging.js'); + registerMessagingHandlers(io as never, socket as never); + + const handler = (socket as EventEmitter).listeners('message_read')[0] as ( + p: unknown, + ) => Promise; + await handler({ conversationId: 'conv-1', lastReadMessageId: 'wrong-msg' }); + + expect(socket.emit).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ + event: 'message_read', + message: expect.stringContaining('Message not found'), + }), + ); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('DB update is called with correct lastReadMessageId', async () => { + const userId = 'user-xyz'; + const lastReadMessageId = 'msg-final'; + + mockFindFirst + .mockResolvedValueOnce({ id: 'm1', userId, conversationId: 'conv-2' }) + .mockResolvedValueOnce({ id: lastReadMessageId, conversationId: 'conv-2' }); + + const setFn = vi.fn().mockReturnThis(); + const whereFn = vi.fn().mockResolvedValue(undefined); + mockUpdate.mockReturnValue({ set: setFn }); + setFn.mockReturnValue({ where: whereFn }); + + const socket = makeSocket(userId); + const io = makeIo(); + + const { registerMessagingHandlers } = await import('../socket/messaging.js'); + registerMessagingHandlers(io as never, socket as never); + + const handler = (socket as EventEmitter).listeners('message_read')[0] as ( + p: unknown, + ) => Promise; + await handler({ conversationId: 'conv-2', lastReadMessageId }); + + expect(setFn).toHaveBeenCalledWith({ lastReadMessageId }); + expect(whereFn).toHaveBeenCalled(); + }); +}); diff --git a/apps/backend/src/__tests__/setup.ts b/apps/backend/src/__tests__/setup.ts new file mode 100644 index 0000000..d48b670 --- /dev/null +++ b/apps/backend/src/__tests__/setup.ts @@ -0,0 +1,2 @@ +process.env['JWT_SECRET'] = 'test-secret-for-ci-only'; +process.env['DATABASE_URL'] = 'postgres://localhost/test'; diff --git a/apps/backend/src/__tests__/stellarListener.test.ts b/apps/backend/src/__tests__/stellarListener.test.ts new file mode 100644 index 0000000..8d57fc5 --- /dev/null +++ b/apps/backend/src/__tests__/stellarListener.test.ts @@ -0,0 +1,171 @@ +/** + * Unit tests for the Stellar event listener (#46). + * + * Each test drives `runForever` with a fake `fetchEvents` so the loop + * exits deterministically — no Soroban RPC, no live DB. The AC the + * tests cover: + * + * - Listener reconnects automatically on disconnect (failure → backoff + * → success on the next poll). + * - Duplicate `tx_hash` entries are ignored (persist is called once per + * event even when the fetcher hands back the same row twice). + * - Errors are logged but do not crash the server (no rethrow out of + * `runForever`). + */ +import { describe, it, expect, vi } from 'vitest'; + +import { runForever, type StellarTransferEvent } from '../services/stellarListener.js'; + +function makeEvent(overrides: Partial = {}): StellarTransferEvent { + return { + txHash: 'tx-1', + ledger: 100, + from: 'GFROM', + to: 'GTO', + amount: '1000', + cursor: 'c1', + ...overrides, + }; +} + +function silentLogger() { + return { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +describe('stellarListener.runForever', () => { + it('persists every event the fetcher returns', async () => { + const events: StellarTransferEvent[][] = [ + [makeEvent({ txHash: 'a', cursor: 'c-a' }), makeEvent({ txHash: 'b', cursor: 'c-b' })], + [makeEvent({ txHash: 'c', cursor: 'c-c' })], + ]; + const persist = vi.fn(async (_event: StellarTransferEvent) => {}); + const ctl = new AbortController(); + let call = 0; + + await runForever({ + log: silentLogger(), + pollIntervalMs: 0, + backoffBaseMs: 1, + backoffMaxMs: 1, + signal: ctl.signal, + persistEvent: persist, + fetchEvents: async () => { + const page = events[call] ?? []; + call += 1; + if (call >= events.length + 1) ctl.abort(); + return page; + }, + }); + + expect(persist).toHaveBeenCalledTimes(3); + expect(persist.mock.calls[0]![0].txHash).toBe('a'); + expect(persist.mock.calls[1]![0].txHash).toBe('b'); + expect(persist.mock.calls[2]![0].txHash).toBe('c'); + }); + + it('reconnects after a fetch failure (backoff, then success)', async () => { + const persist = vi.fn(async (_event: StellarTransferEvent) => {}); + const ctl = new AbortController(); + let call = 0; + + await runForever({ + log: silentLogger(), + pollIntervalMs: 0, + backoffBaseMs: 1, + backoffMaxMs: 1, + signal: ctl.signal, + persistEvent: persist, + fetchEvents: async () => { + call += 1; + if (call === 1) throw new Error('rpc unreachable'); + if (call === 2) { + // Allow the success-path branch to schedule the next poll before + // we abort so the test exercises a real reconnect. + ctl.abort(); + return [makeEvent({ txHash: 'after-reconnect', cursor: 'c-r' })]; + } + return []; + }, + }); + + expect(call).toBeGreaterThanOrEqual(2); + expect(persist).toHaveBeenCalledTimes(1); + expect(persist.mock.calls[0]![0].txHash).toBe('after-reconnect'); + }); + + it('does not crash when persist throws — logs and keeps polling', async () => { + const log = silentLogger(); + const ctl = new AbortController(); + let call = 0; + let persistCalls = 0; + + const persist = vi.fn(async (_event: StellarTransferEvent) => { + persistCalls += 1; + if (persistCalls === 1) { + throw new Error('db unique violation'); + } + }); + + await runForever({ + log, + pollIntervalMs: 0, + backoffBaseMs: 1, + backoffMaxMs: 1, + signal: ctl.signal, + persistEvent: persist, + fetchEvents: async () => { + call += 1; + if (call > 2) { + ctl.abort(); + return []; + } + return [makeEvent({ txHash: `t-${call}`, cursor: `c-${call}` })]; + }, + }); + + // The first persist threw but the loop kept going. + expect(call).toBeGreaterThanOrEqual(2); + expect(persist).toHaveBeenCalledTimes(2); + expect(log.warn).toHaveBeenCalledWith( + 'failed to persist event', + expect.objectContaining({ txHash: 't-1' }), + ); + }); + + it('advances the cursor only on successful persistence', async () => { + const ctl = new AbortController(); + let call = 0; + const cursors: (string | null)[] = []; + + const persist = vi.fn(async (_event: StellarTransferEvent) => { + throw new Error('db down'); + }); + + await runForever({ + log: silentLogger(), + pollIntervalMs: 0, + backoffBaseMs: 1, + backoffMaxMs: 1, + signal: ctl.signal, + persistEvent: persist, + fetchEvents: async (cursor) => { + cursors.push(cursor); + call += 1; + if (call >= 2) { + ctl.abort(); + return []; + } + return [makeEvent({ cursor: 'c-1' })]; + }, + }); + + // First call's cursor is null (initial), second call's cursor is STILL + // null because persist threw and we never advanced. + expect(cursors[0]).toBeNull(); + expect(cursors[1]).toBeNull(); + }); +}); diff --git a/apps/backend/src/__tests__/users.keyBundle.routes.test.ts b/apps/backend/src/__tests__/users.keyBundle.routes.test.ts new file mode 100644 index 0000000..df16718 --- /dev/null +++ b/apps/backend/src/__tests__/users.keyBundle.routes.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; + +const mockFetchBundle = vi.fn(); + +vi.mock('../services/keyBundle.js', () => ({ + fetchAndConsumeKeyBundle: mockFetchBundle, +})); + +vi.mock('../db/index.js', () => ({ + db: { + query: { users: { findFirst: vi.fn(), findMany: vi.fn() } }, + update: vi.fn(), + select: vi.fn(), + }, +})); + +vi.mock('../lib/redis.js', () => ({ + get redis() { + return null; + }, +})); + +vi.mock('../services/presence.js', () => ({ + isOnline: vi.fn(), +})); + +vi.mock('../middleware/auth.js', () => ({ + requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => { + (req as express.Request & { auth: { userId: string } }).auth = { userId: 'caller-1' }; + next(); + }, +})); + +const { usersRouter } = await import('../routes/users.js'); + +function makeApp() { + const app = express(); + app.use(express.json()); + app.use('/users', usersRouter); + return app; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('GET /users/:userId/devices/:deviceId/key-bundle', () => { + it('returns the prekey bundle and forwards the path params', async () => { + const bundle = { + identityPublicKey: 'identity-pub', + registrationId: 4242, + signedPreKey: { keyId: 7, publicKey: 'spk-pub', signature: 'spk-sig' }, + oneTimePreKey: { keyId: 100, publicKey: 'otp-pub' }, + }; + mockFetchBundle.mockResolvedValue({ ok: true, bundle }); + + const res = await request(makeApp()).get('/users/user-1/devices/dev-1/key-bundle'); + + expect(res.status).toBe(200); + expect(res.body).toEqual(bundle); + expect(mockFetchBundle).toHaveBeenCalledWith('user-1', 'dev-1'); + }); + + it('returns a bundle with oneTimePreKey: null when the pool is exhausted', async () => { + mockFetchBundle.mockResolvedValue({ + ok: true, + bundle: { + identityPublicKey: 'identity-pub', + registrationId: 4242, + signedPreKey: { keyId: 7, publicKey: 'spk-pub', signature: 'spk-sig' }, + oneTimePreKey: null, + }, + }); + + const res = await request(makeApp()).get('/users/user-1/devices/dev-1/key-bundle'); + + expect(res.status).toBe(200); + expect(res.body.oneTimePreKey).toBeNull(); + }); + + it('returns 404 for an unknown or revoked device', async () => { + mockFetchBundle.mockResolvedValue({ ok: false, status: 404, error: 'Device not found' }); + + const res = await request(makeApp()).get('/users/user-1/devices/dev-1/key-bundle'); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: 'Device not found' }); + }); + + it('requires authentication', async () => { + // requireAuth is mocked to always authenticate; assert the route sits behind it + // by confirming the handler runs only after auth injected req.auth. + mockFetchBundle.mockResolvedValue({ ok: false, status: 404, error: 'Device not found' }); + + await request(makeApp()).get('/users/user-1/devices/dev-1/key-bundle'); + + expect(mockFetchBundle).toHaveBeenCalled(); + }); +}); diff --git a/apps/backend/src/__tests__/users.test.ts b/apps/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..b6fc90e --- /dev/null +++ b/apps/backend/src/__tests__/users.test.ts @@ -0,0 +1,283 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; +import { signToken } from '../lib/jwt.js'; + +const mockReturning = vi.fn(); +const mockWhere = vi.fn(() => ({ returning: mockReturning })); +const mockSet = vi.fn(() => ({ where: mockWhere })); +const mockUpdate = vi.fn(() => ({ set: mockSet })); + +vi.mock('../db/index.js', () => ({ + db: { + query: { + users: { + findFirst: vi.fn(), + findMany: vi.fn(), + }, + }, + update: mockUpdate, + select: vi.fn(), + }, +})); + +const { usersRouter } = await import('../routes/users.js'); +const { db } = await import('../db/index.js'); + +const app = express(); +app.use(express.json()); +app.use('/users', usersRouter); + +const VALID_TOKEN = signToken({ userId: 'auth-user-id', walletAddress: 'GAUTH' }); +const AUTH_HEADER = `Bearer ${VALID_TOKEN}`; + +const MOCK_USER = { + id: 'user-uuid-123', + username: 'testuser', + avatarUrl: 'https://example.com/avatar.png', + wallets: [ + { address: 'GABCDEFG', isPrimary: true }, + { address: 'GHIJKLMN', isPrimary: false }, + ], +}; + +const MOCK_CREATED_AT = new Date('2026-05-31T12:00:00.000Z'); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('GET /users/me', () => { + it('returns 401 when no Authorization header is provided', async () => { + const res = await request(app).get('/users/me'); + expect(res.status).toBe(401); + }); + + it('returns the authenticated user profile with wallets and createdAt', async () => { + vi.mocked(db.query.users.findFirst).mockResolvedValue({ + id: 'auth-user-id', + username: 'alice', + avatarUrl: null, + wallets: MOCK_USER.wallets, + createdAt: MOCK_CREATED_AT, + } as never); + + const res = await request(app).get('/users/me').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + id: 'auth-user-id', + username: 'alice', + avatarUrl: null, + wallets: [ + { address: 'GABCDEFG', isPrimary: true }, + { address: 'GHIJKLMN', isPrimary: false }, + ], + createdAt: MOCK_CREATED_AT.toISOString(), + }); + }); +}); + +describe('GET /users/:id', () => { + it('returns 401 when no Authorization header is provided', async () => { + const res = await request(app).get('/users/user-uuid-123'); + expect(res.status).toBe(401); + }); + + it('returns 401 when token is invalid', async () => { + const res = await request(app) + .get('/users/user-uuid-123') + .set('Authorization', 'Bearer invalid.token.value'); + expect(res.status).toBe(401); + }); + + it('returns 401 when Authorization header is malformed', async () => { + const res = await request(app) + .get('/users/user-uuid-123') + .set('Authorization', 'NotBearer token'); + expect(res.status).toBe(401); + }); + + it('returns 404 when user does not exist', async () => { + vi.mocked(db.query.users.findFirst).mockResolvedValue(undefined); + + const res = await request(app).get('/users/unknown-uuid').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: 'User not found' }); + }); + + it('returns 404 for a malformed (non-UUID) id', async () => { + vi.mocked(db.query.users.findFirst).mockRejectedValue( + new Error('invalid input syntax for type uuid'), + ); + + const res = await request(app).get('/users/not-a-valid-uuid').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: 'User not found' }); + }); + + it('returns the user profile with wallets on success', async () => { + vi.mocked(db.query.users.findFirst).mockResolvedValue(MOCK_USER as never); + + const res = await request(app).get('/users/user-uuid-123').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(200); + expect(res.body.id).toBe(MOCK_USER.id); + expect(res.body.username).toBe(MOCK_USER.username); + expect(res.body.avatarUrl).toBe(MOCK_USER.avatarUrl); + expect(res.body.wallets).toHaveLength(2); + expect(res.body.wallets[0]).toEqual({ address: 'GABCDEFG', isPrimary: true }); + expect(res.body.wallets[1]).toEqual({ address: 'GHIJKLMN', isPrimary: false }); + }); + + it('strips internal fields even if db returns them', async () => { + const userWithInternals = { + ...MOCK_USER, + createdAt: new Date(), + updatedAt: new Date(), + wallets: MOCK_USER.wallets.map((w) => ({ + ...w, + id: 'wallet-uuid', + userId: 'user-uuid-123', + createdAt: new Date(), + })), + }; + vi.mocked(db.query.users.findFirst).mockResolvedValue(userWithInternals as never); + + const res = await request(app).get('/users/user-uuid-123').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(200); + // Explicit serialization in handler ensures internal fields never reach the response + expect(res.body).not.toHaveProperty('createdAt'); + expect(res.body).not.toHaveProperty('updatedAt'); + expect(res.body.wallets[0]).not.toHaveProperty('id'); + expect(res.body.wallets[0]).not.toHaveProperty('userId'); + expect(res.body.wallets[0]).not.toHaveProperty('createdAt'); + }); +}); + +describe('GET /users/search', () => { + beforeEach(() => { + // The exists() subquery builds `db.select().from().where()` when the handler runs. + const chain = { from: vi.fn(() => chain), where: vi.fn(() => chain) }; + vi.mocked(db.select).mockReturnValue(chain as any); // eslint-disable-line + }); + + it('returns 401 when no token is provided', async () => { + const res = await request(app).get('/users/search?q=test'); + expect(res.status).toBe(401); + }); + + it('returns 400 when q is missing', async () => { + const res = await request(app).get('/users/search').set('Authorization', AUTH_HEADER); + expect(res.status).toBe(400); + }); + + it('returns 400 when q is empty or whitespace', async () => { + const res = await request(app).get('/users/search?q=%20%20').set('Authorization', AUTH_HEADER); + expect(res.status).toBe(400); + }); + + it('returns mapped results with only the primary wallet address', async () => { + vi.mocked(db.query.users.findMany).mockResolvedValue([ + { + id: 'user-uuid-123', + username: 'testuser', + avatarUrl: 'https://example.com/avatar.png', + wallets: [ + { address: 'GABCDEFG', isPrimary: true }, + { address: 'GHIJKLMN', isPrimary: false }, + ], + }, + ] as any); // eslint-disable-line + + const res = await request(app).get('/users/search?q=test').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(200); + expect(res.body).toEqual([ + { + id: 'user-uuid-123', + username: 'testuser', + avatarUrl: 'https://example.com/avatar.png', + primaryWalletAddress: 'GABCDEFG', + }, + ]); + // No private wallet fields leak through. + expect(res.body[0]).not.toHaveProperty('wallets'); + }); + + it('returns null primaryWalletAddress when no primary wallet exists', async () => { + vi.mocked(db.query.users.findMany).mockResolvedValue([ + { id: 'u1', username: 'nowallet', avatarUrl: null, wallets: [] }, + ] as any); // eslint-disable-line + + const res = await request(app).get('/users/search?q=no').set('Authorization', AUTH_HEADER); + + expect(res.status).toBe(200); + expect(res.body[0].primaryWalletAddress).toBeNull(); + }); + + it('caps results at 10 via the query limit', async () => { + vi.mocked(db.query.users.findMany).mockResolvedValue([] as any); // eslint-disable-line + + await request(app).get('/users/search?q=test').set('Authorization', AUTH_HEADER); + + expect(vi.mocked(db.query.users.findMany)).toHaveBeenCalledWith( + expect.objectContaining({ limit: 10 }), + ); + }); +}); + +describe('PATCH /users/me', () => { + it('returns 401 when no token is provided', async () => { + const res = await request(app).patch('/users/me').send({ username: 'valid_name' }); + expect(res.status).toBe(401); + }); + + it('returns 400 for invalid username format', async () => { + const res = await request(app) + .patch('/users/me') + .set('Authorization', AUTH_HEADER) + .send({ username: 'ab' }); // too short + + expect(res.status).toBe(400); + expect(res.body.error).toContain('Username must be 3-30'); + }); + + it('returns 409 for duplicate username', async () => { + vi.mocked(db.query.users.findFirst).mockResolvedValue({ + id: 'another-user-id', + username: 'conflict', + } as never); + + const res = await request(app) + .patch('/users/me') + .set('Authorization', AUTH_HEADER) + .send({ username: 'conflict' }); + + expect(res.status).toBe(409); + expect(res.body.error).toBe('Username is already taken'); + }); + + it('returns 200 and updated user on success', async () => { + vi.mocked(db.query.users.findFirst).mockResolvedValue(undefined); // no conflict + + const mockReturning = vi + .fn() + .mockResolvedValue([{ id: 'auth-user-id', username: 'new_name', avatarUrl: 'new_url' }]); + const mockWhere = vi.fn(() => ({ returning: mockReturning })); + const mockSet = vi.fn(() => ({ where: mockWhere })); + vi.mocked(db.update).mockReturnValue({ set: mockSet } as never); + + const res = await request(app) + .patch('/users/me') + .set('Authorization', AUTH_HEADER) + .send({ username: 'new_name', avatarUrl: 'new_url' }); + + expect(res.status).toBe(200); + expect(res.body.username).toBe('new_name'); + expect(res.body.avatarUrl).toBe('new_url'); + }); +}); diff --git a/apps/backend/src/__tests__/validate.test.ts b/apps/backend/src/__tests__/validate.test.ts new file mode 100644 index 0000000..fd1b09a --- /dev/null +++ b/apps/backend/src/__tests__/validate.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest'; +import express, { type Request, type Response } from 'express'; +import request from 'supertest'; +import { z } from 'zod'; +import { validate } from '../middleware/validate.js'; + +const TestSchema = z.object({ + name: z.string().min(1, 'name is required'), + age: z.number().int('age must be an integer'), +}); + +function makeApp() { + const app = express(); + app.use(express.json()); + app.post('/test', validate(TestSchema), (req: Request, res: Response) => { + res.json({ received: req.body }); + }); + return app; +} + +describe('validate middleware', () => { + const app = makeApp(); + + it('calls next and passes body through on valid input', async () => { + const res = await request(app).post('/test').send({ name: 'Alice', age: 30 }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ received: { name: 'Alice', age: 30 } }); + }); + + it('returns 400 with structured error on missing required field', async () => { + const res = await request(app).post('/test').send({ age: 25 }); + expect(res.status).toBe(400); + expect(res.body.error).toBe('Validation failed'); + expect(Array.isArray(res.body.issues)).toBe(true); + const fields = res.body.issues.map((i: { field: string }) => i.field); + expect(fields).toContain('name'); + }); + + it('returns 400 with structured error on wrong type', async () => { + const res = await request(app).post('/test').send({ name: 'Bob', age: 'not-a-number' }); + expect(res.status).toBe(400); + expect(res.body.error).toBe('Validation failed'); + expect(res.body.issues[0]).toHaveProperty('field'); + expect(res.body.issues[0]).toHaveProperty('message'); + }); + + it('returns 400 with error for empty body', async () => { + const res = await request(app).post('/test').send({}); + expect(res.status).toBe(400); + expect(res.body.error).toBe('Validation failed'); + expect(res.body.issues.length).toBeGreaterThan(0); + }); + + it('issues array entries have field and message keys', async () => { + const res = await request(app).post('/test').send({ age: 10 }); + expect(res.status).toBe(400); + for (const issue of res.body.issues as { field: string; message: string }[]) { + expect(issue).toHaveProperty('field'); + expect(issue).toHaveProperty('message'); + expect(typeof issue.field).toBe('string'); + expect(typeof issue.message).toBe('string'); + } + }); +}); + +describe('auth route validation via validate middleware', () => { + it('validate middleware integrates as Express RequestHandler', () => { + const handler = validate(TestSchema); + expect(typeof handler).toBe('function'); + // Ensure it accepts (req, res, next) signature + expect(handler.length).toBe(3); + }); +}); diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts new file mode 100644 index 0000000..ede35cf --- /dev/null +++ b/apps/backend/src/app.ts @@ -0,0 +1,53 @@ +import express from 'express'; +import type { Express } from 'express'; +import cors from 'cors'; +import morgan from 'morgan'; +import { readFileSync } from 'node:fs'; +import { sql } from 'drizzle-orm'; +import { db } from './db/index.js'; +import { authRouter } from './routes/auth.js'; +import { conversationsRouter } from './routes/conversations.js'; +import { messagesRouter } from './routes/messages.js'; +import { usersRouter } from './routes/users.js'; +import { requireAuth, type AuthRequest } from './middleware/auth.js'; + +const packageJson = JSON.parse( + readFileSync(new URL('../package.json', import.meta.url), 'utf8'), +) as { version: string }; + +export const app: Express = express(); + +app.use(cors()); +app.use(express.json()); +if (process.env['NODE_ENV'] !== 'test') { + app.use(morgan('dev')); +} + +app.get('/health', async (_req, res) => { + const health = { + status: 'ok' as const, + db: 'connected' as const, + node: process.version, + version: packageJson.version, + }; + + try { + await db.execute(sql`SELECT 1`); + res.json(health); + } catch { + res.status(503).json({ + ...health, + status: 'error', + db: 'unreachable', + }); + } +}); + +app.use('/auth', authRouter); +app.use('/conversations', conversationsRouter); +app.use('/messages', messagesRouter); +app.use('/users', usersRouter); + +app.get('/me', requireAuth, (req, res) => { + res.json({ user: (req as AuthRequest).auth }); +}); diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts new file mode 100644 index 0000000..ce53b6c --- /dev/null +++ b/apps/backend/src/config.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +/** + * Startup environment schema. Every variable here is required for the + * backend to boot; `loadEnv` validates `process.env` against it and exits + * the process if anything is missing or malformed. + */ +export const EnvSchema = z.object({ + DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'), + REDIS_URL: z.string().min(1, 'REDIS_URL is required'), + JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'), + PORT: z.coerce.number().int('PORT must be an integer').positive('PORT must be positive'), + TOKEN_TRANSFER_CONTRACT_ID: z.string().min(1, 'TOKEN_TRANSFER_CONTRACT_ID is required'), +}); + +export type Env = z.infer; + +/** + * Validate the given environment (defaults to `process.env`) against + * `EnvSchema`. On success returns the parsed, typed env and emits no + * output. On failure it logs the offending variables and exits with code 1. + * + * The `source` parameter exists so tests can stub the environment without + * mutating the real `process.env`. + */ +export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env { + const result = EnvSchema.safeParse(source); + + if (!result.success) { + const vars = [...new Set(result.error.issues.map((issue) => issue.path.join('.')))]; + console.error(`Missing or invalid environment variables: ${vars.join(', ')}`); + for (const issue of result.error.issues) { + console.error(` - ${issue.path.join('.')}: ${issue.message}`); + } + process.exit(1); + } + + return result.data; +} diff --git a/apps/backend/src/constants.ts b/apps/backend/src/constants.ts new file mode 100644 index 0000000..23f3439 --- /dev/null +++ b/apps/backend/src/constants.ts @@ -0,0 +1,2 @@ +export const MAX_MESSAGES_LIMIT = 50; +export const DEFAULT_MESSAGES_LIMIT = 30; diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts index 00a1405..ccc8e0e 100644 --- a/apps/backend/src/db/schema.ts +++ b/apps/backend/src/db/schema.ts @@ -1,5 +1,15 @@ -import { pgTable, text, timestamp, uuid, boolean, pgEnum } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; +import { + pgTable, + text, + timestamp, + uuid, + boolean, + integer, + pgEnum, + index, + unique, +} from 'drizzle-orm/pg-core'; +import { relations, sql } from 'drizzle-orm'; export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), @@ -19,6 +29,57 @@ export const wallets = pgTable('wallets', { createdAt: timestamp('created_at').notNull().defaultNow(), }); +// ─── Devices & E2E prekey bundles (#160) ─────────────────────────────────────── +// +// Every device advertises an X3DH/Signal-style key bundle so other users can +// open an end-to-end encrypted session with it: +// - a long-term `identityPublicKey` + numeric `registrationId` +// - one medium-term signed prekey (`signedPreKey*`), and +// - a pool of single-use one-time prekeys (`one_time_pre_keys`). +// +// Only PUBLIC key material and signatures are stored here — private keys never +// leave the owning client. A one-time prekey is handed out at most once: it is +// claimed with a single atomic `UPDATE ... WHERE consumed = false ... RETURNING` +// so concurrent senders can never receive the same key. `revokedAt` soft-revokes +// a device, after which its bundle is no longer served. + +export const devices = pgTable( + 'devices', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + identityPublicKey: text('identity_public_key').notNull(), + registrationId: integer('registration_id').notNull(), + signedPreKeyId: integer('signed_pre_key_id').notNull(), + signedPreKeyPublic: text('signed_pre_key_public').notNull(), + signedPreKeySignature: text('signed_pre_key_signature').notNull(), + revokedAt: timestamp('revoked_at'), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => [index('devices_user_id_idx').on(table.userId)], +); + +export const oneTimePreKeys = pgTable( + 'one_time_pre_keys', + { + id: uuid('id').primaryKey().defaultRandom(), + deviceId: uuid('device_id') + .notNull() + .references(() => devices.id, { onDelete: 'cascade' }), + keyId: integer('key_id').notNull(), + publicKey: text('public_key').notNull(), + consumed: boolean('consumed').notNull().default(false), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => [ + // Partial-friendly lookup of the next unconsumed key for a device. + index('one_time_pre_keys_device_consumed_idx').on(table.deviceId, table.consumed), + unique('one_time_pre_keys_device_key_unique').on(table.deviceId, table.keyId), + ], +); + // ─── Conversations ──────────────────────────────────────────────────────────── export const conversationTypeEnum = pgEnum('conversation_type', ['dm', 'group']); @@ -27,6 +88,7 @@ export const conversations = pgTable('conversations', { id: uuid('id').primaryKey().defaultRandom(), type: conversationTypeEnum('type').notNull().default('dm'), name: text('name'), + avatarUrl: text('avatar_url'), createdAt: timestamp('created_at').notNull().defaultNow(), }); @@ -38,10 +100,43 @@ export const conversationMembers = pgTable('conversation_members', { userId: uuid('user_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), + lastReadMessageId: uuid('last_read_message_id').references(() => messages.id, { + onDelete: 'set null', + }), + isMuted: boolean('is_muted').notNull().default(false), + isArchived: boolean('is_archived').notNull().default(false), joinedAt: timestamp('joined_at').notNull().defaultNow(), }); -export const messages = pgTable('messages', { +export const messages = pgTable( + 'messages', + { + id: uuid('id').primaryKey().defaultRandom(), + conversationId: uuid('conversation_id') + .notNull() + .references(() => conversations.id, { onDelete: 'cascade' }), + senderId: uuid('sender_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + content: text('content').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + deletedAt: timestamp('deleted_at'), + }, + (table) => [ + index('messages_content_search_idx').using( + 'gin', + sql`to_tsvector('english', ${table.content})`, + ), + ], +); + +// ─── Token transfers (#46) ──────────────────────────────────────────────────── +// +// One row per Soroban `transfer` event the listener (services/stellarListener.ts) +// pulls off the contract. The `txHash` is unique so reconnects + replayed event +// pages upsert cleanly instead of producing duplicates. + +export const tokenTransfers = pgTable('token_transfers', { id: uuid('id').primaryKey().defaultRandom(), conversationId: uuid('conversation_id') .notNull() @@ -49,7 +144,11 @@ export const messages = pgTable('messages', { senderId: uuid('sender_id') .notNull() .references(() => users.id, { onDelete: 'cascade' }), - content: text('content').notNull(), + recipientAddress: text('recipient_address').notNull(), + amount: text('amount').notNull(), + tokenContractId: text('token_contract_id').notNull(), + txHash: text('tx_hash').notNull().unique(), + memo: text('memo'), createdAt: timestamp('created_at').notNull().defaultNow(), }); @@ -59,15 +158,27 @@ export const usersRelations = relations(users, ({ many }) => ({ wallets: many(wallets), memberships: many(conversationMembers), messages: many(messages), + transfers: many(tokenTransfers), + devices: many(devices), })); export const walletsRelations = relations(wallets, ({ one }) => ({ user: one(users, { fields: [wallets.userId], references: [users.id] }), })); +export const devicesRelations = relations(devices, ({ one, many }) => ({ + user: one(users, { fields: [devices.userId], references: [users.id] }), + oneTimePreKeys: many(oneTimePreKeys), +})); + +export const oneTimePreKeysRelations = relations(oneTimePreKeys, ({ one }) => ({ + device: one(devices, { fields: [oneTimePreKeys.deviceId], references: [devices.id] }), +})); + export const conversationsRelations = relations(conversations, ({ many }) => ({ members: many(conversationMembers), messages: many(messages), + transfers: many(tokenTransfers), })); export const conversationMembersRelations = relations(conversationMembers, ({ one }) => ({ @@ -86,6 +197,17 @@ export const messagesRelations = relations(messages, ({ one }) => ({ sender: one(users, { fields: [messages.senderId], references: [users.id] }), })); +export const tokenTransfersRelations = relations(tokenTransfers, ({ one }) => ({ + conversation: one(conversations, { + fields: [tokenTransfers.conversationId], + references: [conversations.id], + }), + sender: one(users, { + fields: [tokenTransfers.senderId], + references: [users.id], + }), +})); + // ─── Types ──────────────────────────────────────────────────────────────────── export type User = typeof users.$inferSelect; @@ -97,3 +219,9 @@ export type NewConversation = typeof conversations.$inferInsert; export type ConversationMember = typeof conversationMembers.$inferSelect; export type Message = typeof messages.$inferSelect; export type NewMessage = typeof messages.$inferInsert; +export type TokenTransfer = typeof tokenTransfers.$inferSelect; +export type NewTokenTransfer = typeof tokenTransfers.$inferInsert; +export type Device = typeof devices.$inferSelect; +export type NewDevice = typeof devices.$inferInsert; +export type OneTimePreKey = typeof oneTimePreKeys.$inferSelect; +export type NewOneTimePreKey = typeof oneTimePreKeys.$inferInsert; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 642150a..63ac88b 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,57 +1,139 @@ -import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; import { createServer } from 'http'; import { Server } from 'socket.io'; -import morgan from 'morgan'; -import { sql } from 'drizzle-orm'; +import { createAdapter } from '@socket.io/redis-adapter'; +import { createClient } from 'redis'; +import dotenv from 'dotenv'; +import { eq } from 'drizzle-orm'; import { db } from './db/index.js'; -import { authRouter } from './routes/auth.js'; -import { conversationsRouter } from './routes/conversations.js'; -import { requireAuth } from './middleware/auth.js'; +import { conversationMembers } from './db/schema.js'; import { socketAuthMiddleware, type AuthSocket } from './middleware/socketAuth.js'; import { registerMessagingHandlers } from './socket/messaging.js'; +import { app } from './app.js'; +import { redis as appRedis } from './lib/redis.js'; +import { setSocketServer } from './lib/socket.js'; +import { setOnline, setOffline, refreshPresence } from './services/presence.js'; +import { buildRpcFetcher, runForever as runStellarListener } from './services/stellarListener.js'; +import { loadEnv } from './config.js'; dotenv.config(); -const app = express(); +// Validate required environment variables at boot. Exits with code 1 and +// logs the offending vars if anything is missing or malformed. +loadEnv(); + const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: '*' }, }); -app.use(cors()); -app.use(express.json()); -app.use(morgan('dev')); +setSocketServer(io); -app.get('/health', async (_req, res) => { - try { - await db.execute(sql`SELECT 1`); - res.json({ status: 'ok', db: 'connected' }); - } catch { - res.status(503).json({ status: 'error', db: 'unreachable' }); - } -}); +io.use(socketAuthMiddleware); -app.use('/auth', authRouter); -app.use('/conversations', conversationsRouter); +io.on('connection', async (socket: AuthSocket) => { + const userId = socket.auth!.userId; + console.log('User connected:', userId, socket.id); -// Protected route example -app.get('/me', requireAuth, (req, res) => { - res.json({ user: req.auth }); -}); + // Auto-join all conversation rooms so the socket receives new_message events + // for every conversation the user belongs to (needed for unread badge tracking). + const memberships = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.userId, userId), + columns: { conversationId: true }, + }); + for (const m of memberships) { + await socket.join(m.conversationId); + } -io.use(socketAuthMiddleware); + if (appRedis) { + await setOnline(appRedis, userId, socket.id); + for (const m of memberships) { + io.to(m.conversationId).emit('user_online', { userId }); + io.to(m.conversationId).emit('presence_update', { userId, online: true }); + } + } + + socket.on('heartbeat', async () => { + if (appRedis) { + await refreshPresence(appRedis, userId); + } + }); -io.on('connection', (socket: AuthSocket) => { - console.log('User connected:', socket.auth?.userId, socket.id); registerMessagingHandlers(io, socket); - socket.on('disconnect', () => { - console.log('User disconnected:', socket.auth?.userId); + + socket.on('disconnect', async () => { + console.log('User disconnected:', userId); + if (appRedis) { + const fullyOffline = await setOffline(appRedis, userId, socket.id); + if (fullyOffline) { + const memberships = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.userId, userId), + columns: { conversationId: true }, + }); + for (const m of memberships) { + io.to(m.conversationId).emit('user_offline', { userId }); + io.to(m.conversationId).emit('presence_update', { userId, online: false }); + } + } + } }); }); +/** + * Issue #7 — Redis pub/sub adapter for horizontal Socket.IO scaling. + * + * When `REDIS_URL` is reachable, attach `@socket.io/redis-adapter` so + * multiple backend instances share rooms via Redis pub/sub. If the + * connection fails (Redis down, wrong URL, or env var unset), log a + * warning and continue running in single-instance mode — the in-process + * adapter remains active so the server still works locally. + */ +async function attachRedisAdapter(): Promise { + const redisUrl = process.env['REDIS_URL'] ?? 'redis://localhost:6379'; + const pubClient = createClient({ url: redisUrl }); + const subClient = pubClient.duplicate(); + + pubClient.on('error', (err) => { + console.warn('[socket.io] Redis pub client error — degrading to local adapter:', err.message); + }); + subClient.on('error', (err) => { + console.warn('[socket.io] Redis sub client error — degrading to local adapter:', err.message); + }); + + try { + await Promise.all([pubClient.connect(), subClient.connect()]); + io.adapter(createAdapter(pubClient, subClient)); + console.log(`[socket.io] Redis adapter attached (${redisUrl})`); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.warn(`[socket.io] Redis unavailable (${message}) — running in single-instance mode`); + await Promise.allSettled([pubClient.quit(), subClient.quit()]); + } +} + const PORT = process.env['PORT'] ?? 3001; httpServer.listen(PORT, () => { console.log(`Backend server running on port ${PORT}`); }); + +// Attach the Redis adapter after listen() so the API is reachable even if +// Redis is unreachable; on failure we fall back to the in-process adapter. +void attachRedisAdapter(); + +// #46 — Stellar transfer event listener. Only spin up when the contract +// id is configured so local-dev and unit-test runs don't try to talk to +// Soroban RPC. The listener never throws out of runForever, so a failed +// chain connection logs but doesn't crash the API. +const stellarRpcUrl = process.env['STELLAR_RPC_URL']; +const tokenTransferContractId = process.env['TOKEN_TRANSFER_CONTRACT_ID']; +if (stellarRpcUrl && tokenTransferContractId) { + void runStellarListener({ + fetchEvents: buildRpcFetcher({ + rpcUrl: stellarRpcUrl, + contractId: tokenTransferContractId, + }), + }); +} else { + console.log( + '[stellar-listener] STELLAR_RPC_URL or TOKEN_TRANSFER_CONTRACT_ID unset; listener disabled.', + ); +} diff --git a/apps/backend/src/lib/conversationCache.ts b/apps/backend/src/lib/conversationCache.ts new file mode 100644 index 0000000..8f04425 --- /dev/null +++ b/apps/backend/src/lib/conversationCache.ts @@ -0,0 +1,10 @@ +import { convCacheKey, redis } from './redis.js'; + +export async function invalidateConversationCaches(userIds: string[]): Promise { + if (!redis || userIds.length === 0) { + return; + } + + const client = redis; + await Promise.allSettled([...new Set(userIds)].map((userId) => client.del(convCacheKey(userId)))); +} diff --git a/apps/backend/src/lib/jwt.ts b/apps/backend/src/lib/jwt.ts index 941b7b9..b9bf3ba 100644 --- a/apps/backend/src/lib/jwt.ts +++ b/apps/backend/src/lib/jwt.ts @@ -6,15 +6,17 @@ if (!SECRET) { throw new Error('JWT_SECRET is not set'); } +const JWT_SECRET: string = SECRET; + export interface JwtPayload { userId: string; walletAddress: string; } export function signToken(payload: JwtPayload): string { - return jwt.sign(payload, SECRET, { expiresIn: '7d' }); + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }); } export function verifyToken(token: string): JwtPayload { - return jwt.verify(token, SECRET) as JwtPayload; + return jwt.verify(token, JWT_SECRET) as unknown as JwtPayload; } diff --git a/apps/backend/src/lib/messages.ts b/apps/backend/src/lib/messages.ts new file mode 100644 index 0000000..a07cb4c --- /dev/null +++ b/apps/backend/src/lib/messages.ts @@ -0,0 +1,15 @@ +type MessageLike = { + content: string | null; + deletedAt?: Date | null; +}; + +export function serializeMessage( + message: T, +): Omit & { content: string | null } { + const { deletedAt, ...rest } = message; + + return { + ...rest, + content: deletedAt ? null : message.content, + }; +} diff --git a/apps/backend/src/lib/nonce.ts b/apps/backend/src/lib/nonce.ts index 68626c3..c4599ec 100644 --- a/apps/backend/src/lib/nonce.ts +++ b/apps/backend/src/lib/nonce.ts @@ -1,6 +1,5 @@ import { randomBytes } from 'crypto'; -// Nonces expire after 5 minutes const TTL_MS = 5 * 60 * 1000; const store = new Map(); diff --git a/apps/backend/src/lib/redis.ts b/apps/backend/src/lib/redis.ts new file mode 100644 index 0000000..2bd7bb2 --- /dev/null +++ b/apps/backend/src/lib/redis.ts @@ -0,0 +1,16 @@ +import { Redis } from 'ioredis'; + +export let redis: Redis | null = null; + +if (process.env['REDIS_URL']) { + redis = new Redis(process.env['REDIS_URL'], { lazyConnect: true }); + redis.on('error', () => { + // Graceful degradation: cache misses fall through to DB + }); +} + +export const CONV_CACHE_TTL = 30; // seconds + +export function convCacheKey(userId: string): string { + return `conversations:${userId}`; +} diff --git a/apps/backend/src/lib/socket.ts b/apps/backend/src/lib/socket.ts new file mode 100644 index 0000000..b0832be --- /dev/null +++ b/apps/backend/src/lib/socket.ts @@ -0,0 +1,11 @@ +import type { Server } from 'socket.io'; + +let socketServer: Server | null = null; + +export function setSocketServer(server: Server): void { + socketServer = server; +} + +export function getSocketServer(): Server | null { + return socketServer; +} diff --git a/apps/backend/src/middleware/socketAuth.ts b/apps/backend/src/middleware/socketAuth.ts index 38446cf..5f1ace5 100644 --- a/apps/backend/src/middleware/socketAuth.ts +++ b/apps/backend/src/middleware/socketAuth.ts @@ -5,10 +5,7 @@ export interface AuthSocket extends Socket { auth?: JwtPayload; } -export function socketAuthMiddleware( - socket: AuthSocket, - next: (err?: Error) => void, -): void { +export function socketAuthMiddleware(socket: AuthSocket, next: (err?: Error) => void): void { const token = socket.handshake.auth['token'] as string | undefined; if (!token) { diff --git a/apps/backend/src/middleware/validate.ts b/apps/backend/src/middleware/validate.ts new file mode 100644 index 0000000..91db291 --- /dev/null +++ b/apps/backend/src/middleware/validate.ts @@ -0,0 +1,20 @@ +import type { Request, Response, NextFunction } from 'express'; +import type { z } from 'zod'; + +export function validate(schema: z.ZodTypeAny) { + return (req: Request, res: Response, next: NextFunction): void => { + const result = schema.safeParse(req.body); + if (!result.success) { + res.status(400).json({ + error: 'Validation failed', + issues: result.error.issues.map((i: z.ZodIssue) => ({ + field: i.path.join('.') || 'unknown', + message: i.message, + })), + }); + return; + } + req.body = result.data as unknown; + next(); + }; +} diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index 353d4f3..c7b71ba 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -1,84 +1,116 @@ +import { createHash } from 'node:crypto'; import { Router } from 'express'; +import type { Request, Response, IRouter } from 'express'; +import rateLimit, { type RateLimitRequestHandler } from 'express-rate-limit'; import { Keypair } from '@stellar/stellar-sdk'; import { db } from '../db/index.js'; import { users, wallets } from '../db/schema.js'; import { eq } from 'drizzle-orm'; import { createNonce, consumeNonce } from '../lib/nonce.js'; import { signToken } from '../lib/jwt.js'; +import { validate } from '../middleware/validate.js'; +import { + ChallengeSchema, + VerifySchema, + type ChallengeBody, + type VerifyBody, +} from '../schemas/auth.schemas.js'; -export const authRouter = Router(); +export const authRouter: IRouter = Router(); -// Step 1: client requests a challenge nonce for a wallet address -authRouter.post('/challenge', (req, res) => { - const { walletAddress } = req.body as { walletAddress?: string }; - - if (!walletAddress) { - res.status(400).json({ error: 'walletAddress is required' }); - return; - } +const rateLimitedResponse = { error: 'Too many requests' }; - const nonce = createNonce(walletAddress); - const message = `Sign in to Clicked\nWallet: ${walletAddress}\nNonce: ${nonce}`; +export const challengeLimiter: RateLimitRequestHandler = rateLimit({ + windowMs: 60 * 1000, + limit: 10, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: rateLimitedResponse, +}); - res.json({ message, nonce }); +export const verifyLimiter: RateLimitRequestHandler = rateLimit({ + windowMs: 60 * 1000, + limit: 5, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: rateLimitedResponse, }); -// Step 2: client signs the message and submits the signature -authRouter.post('/verify', async (req, res) => { - const { walletAddress, signature, nonce } = req.body as { - walletAddress?: string; - signature?: string; - nonce?: string; - }; - - if (!walletAddress || !signature || !nonce) { - res.status(400).json({ error: 'walletAddress, signature, and nonce are required' }); - return; - } - - // Validate and consume nonce - const valid = consumeNonce(walletAddress, nonce); - if (!valid) { - res.status(401).json({ error: 'Invalid or expired nonce' }); - return; - } - - // Verify Stellar keypair signature - try { +// Step 1: client requests a challenge nonce for a wallet address +authRouter.post( + '/challenge', + challengeLimiter, + validate(ChallengeSchema), + (req: Request, res: Response) => { + const { walletAddress } = req.body as ChallengeBody; + + const nonce = createNonce(walletAddress); const message = `Sign in to Clicked\nWallet: ${walletAddress}\nNonce: ${nonce}`; - const messageBytes = Buffer.from(message); - const signatureBytes = Buffer.from(signature, 'hex'); - const keypair = Keypair.fromPublicKey(walletAddress); - if (!keypair.verify(messageBytes, signatureBytes)) { - res.status(401).json({ error: 'Signature verification failed' }); + res.json({ message, nonce }); + }, +); + +// Step 2: client signs the message and submits the signature +authRouter.post( + '/verify', + verifyLimiter, + validate(VerifySchema), + async (req: Request, res: Response) => { + const { walletAddress, signature, nonce } = req.body as VerifyBody; + + // Validate and consume nonce + const valid = consumeNonce(walletAddress, nonce); + if (!valid) { + res.status(401).json({ error: 'Invalid or expired nonce' }); return; } - } catch { - res.status(401).json({ error: 'Invalid signature or wallet address' }); - return; - } - - // Upsert user + wallet - let userId: string; - - const existingWallet = await db.query.wallets.findFirst({ - where: eq(wallets.address, walletAddress), - with: { user: true }, - }); - - if (existingWallet) { - userId = existingWallet.userId; - } else { - const [newUser] = await db.insert(users).values({}).returning({ id: users.id }); - if (!newUser) { - res.status(500).json({ error: 'Failed to create user' }); + + // Verify Stellar keypair signature + try { + const message = `Sign in to Clicked\nWallet: ${walletAddress}\nNonce: ${nonce}`; + const rawMessageBytes = Buffer.from(message); + const freighterMessageBytes = createHash('sha256') + .update(`Stellar Signed Message:\n${message}`) + .digest(); + const keypair = Keypair.fromPublicKey(walletAddress); + const hexSignatureBytes = Buffer.from(signature, 'hex'); + const base64SignatureBytes = Buffer.from(signature, 'base64'); + + const isValidSignature = + keypair.verify(rawMessageBytes, hexSignatureBytes) || + keypair.verify(freighterMessageBytes, base64SignatureBytes); + + if (!isValidSignature) { + res.status(401).json({ error: 'Signature verification failed' }); + return; + } + } catch { + res.status(401).json({ error: 'Invalid signature or wallet address' }); return; } - userId = newUser.id; - await db.insert(wallets).values({ userId, address: walletAddress, isPrimary: true }); - } - const token = signToken({ userId, walletAddress }); - res.json({ token }); -}); + // Upsert user + wallet + let userId: string; + + const existingWallet = await db.query.wallets.findFirst({ + where: eq(wallets.address, walletAddress), + with: { user: true }, + }); + + if (existingWallet) { + userId = existingWallet.userId; + } else { + const [newUser] = await db.insert(users).values({}).returning({ id: users.id }); + if (!newUser) { + res.status(500).json({ error: 'Failed to create user' }); + return; + } + userId = newUser.id; + await db.insert(wallets).values({ userId, address: walletAddress, isPrimary: true }); + } + + const token = signToken({ userId, walletAddress }); + res.json({ token }); + }, +); diff --git a/apps/backend/src/routes/conversations.ts b/apps/backend/src/routes/conversations.ts index 72d1954..822070f 100644 --- a/apps/backend/src/routes/conversations.ts +++ b/apps/backend/src/routes/conversations.ts @@ -1,26 +1,765 @@ import { Router } from 'express'; -import { eq } from 'drizzle-orm'; +import type { IRouter } from 'express'; +import { asc, and, count, desc, eq, lt, sql, ne } from 'drizzle-orm'; import { db } from '../db/index.js'; -import { conversationMembers } from '../db/schema.js'; +import { conversationMembers, conversations, messages, tokenTransfers } from '../db/schema.js'; import { requireAuth, type AuthRequest } from '../middleware/auth.js'; +import { redis, CONV_CACHE_TTL, convCacheKey } from '../lib/redis.js'; +import { invalidateConversationCaches } from '../lib/conversationCache.js'; +import { serializeMessage } from '../lib/messages.js'; +import { getSocketServer } from '../lib/socket.js'; +import { MAX_MESSAGES_LIMIT, DEFAULT_MESSAGES_LIMIT } from '../constants.js'; -export const conversationsRouter = Router(); +export const conversationsRouter: IRouter = Router(); conversationsRouter.use(requireAuth); +const SEARCH_RESULT_LIMIT = 20; + +const conversationRelations = { + members: { + with: { + user: { + columns: { id: true, username: true, avatarUrl: true }, + with: { wallets: { columns: { address: true, isPrimary: true } } }, + }, + }, + }, + messages: { + orderBy: desc(messages.createdAt), + limit: 1, + with: { sender: { columns: { id: true, username: true, avatarUrl: true } } }, + }, +} as const; + +type ConversationPayload = { + messages?: Array>; + [key: string]: unknown; +}; + +function serializeConversation(conversation: T): T { + return { + ...conversation, + messages: (conversation.messages ?? []).map((message) => serializeMessage(message)), + }; +} + +type ConversationMemberPayload = { + joinedAt: Date; + user: { + id: string; + username: string | null; + avatarUrl: string | null; + wallets: Array<{ address: string; isPrimary: boolean }>; + }; +}; + +function serializeConversationMember(member: ConversationMemberPayload) { + return { + id: member.user.id, + username: member.user.username, + avatarUrl: member.user.avatarUrl, + primaryWalletAddress: + member.user.wallets.find((wallet) => wallet.isPrimary)?.address ?? + member.user.wallets[0]?.address ?? + null, + joinedAt: member.joinedAt, + }; +} + // List all conversations the authenticated user belongs to +// Pass ?archived=true to include archived conversations conversationsRouter.get('/', async (req: AuthRequest, res) => { const userId = req.auth!.userId; + const showArchived = req.query['archived'] === 'true'; + const key = convCacheKey(userId); + + // Cache read — skip when requesting archived (different result set) + if (!showArchived && redis) { + try { + const cached = await redis.get(key); + if (cached) { + res.json(JSON.parse(cached) as unknown); + return; + } + } catch { + // Fall through to DB on Redis error + } + } + + const memberships = (await db.query.conversationMembers.findMany({ + where: and( + eq(conversationMembers.userId, userId), + showArchived ? undefined : ne(conversationMembers.isArchived, true), + ), + with: { + conversation: conversationRelations as never, + }, + })) as unknown as Array<{ conversationId: string; conversation: ConversationPayload }>; + + // Single subquery for message counts — no N+1 + const conversationIds = memberships.map((m) => m.conversationId); + const countRows = + conversationIds.length > 0 + ? await db + .select({ conversationId: messages.conversationId, count: count() }) + .from(messages) + .where( + sql`${messages.conversationId} = ANY(ARRAY[${sql.join( + conversationIds.map((id) => sql`${id}::uuid`), + sql`, `, + )}])`, + ) + .groupBy(messages.conversationId) + : []; + + const countMap = new Map(countRows.map((r) => [r.conversationId, r.count])); + + // Unread count per conversation: messages after the member's lastReadMessageId. + // Returns 0 when lastReadMessageId is NULL (no read position established yet). + const unreadRows: Array<{ conversationId: string; unreadCount: number }> = + conversationIds.length > 0 + ? [ + ...(await db.execute<{ conversationId: string; unreadCount: number }>(sql` + SELECT + cm.conversation_id AS "conversationId", + CASE + WHEN cm.last_read_message_id IS NULL THEN 0 + ELSE ( + SELECT COUNT(*)::int + FROM messages m2 + WHERE m2.conversation_id = cm.conversation_id + AND m2.deleted_at IS NULL + AND m2.created_at > lrm.created_at + ) + END AS "unreadCount" + FROM conversation_members cm + LEFT JOIN messages lrm ON lrm.id = cm.last_read_message_id + WHERE cm.user_id = ${userId}::uuid + AND cm.conversation_id = ANY(ARRAY[${sql.join( + conversationIds.map((id) => sql`${id}::uuid`), + sql`, `, + )}]) + `)), + ] + : []; + + const unreadMap = new Map(unreadRows.map((r) => [r.conversationId, r.unreadCount])); + + const result = memberships.map((m) => ({ + ...m.conversation, + isMuted: m.isMuted, + isArchived: m.isArchived, + messageCount: countMap.get(m.conversationId) ?? 0, + unreadCount: unreadMap.get(m.conversationId) ?? 0, + })); + + // Cache write with 30-second TTL (only for default non-archived view) + if (!showArchived && redis) { + try { + await redis.setex(key, CONV_CACHE_TTL, JSON.stringify(result)); + } catch { + // Ignore — response is already computed + } + } + + res.json(result); +}); + +conversationsRouter.get('/:id', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + const conversation = (await db.query.conversations.findFirst({ + where: eq(conversations.id, conversationId), + with: conversationRelations as never, + })) as ConversationPayload | undefined; + + if (!conversation) { + res.status(404).json({ error: 'Conversation not found' }); + return; + } + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + res.json(serializeConversation(conversation)); +}); + +conversationsRouter.get('/:id/members', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } - const memberships = await db.query.conversationMembers.findMany({ - where: eq(conversationMembers.userId, userId), + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + const members = (await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, conversationId), + orderBy: asc(conversationMembers.joinedAt), + columns: { + joinedAt: true, + }, with: { - conversation: { - with: { members: { with: { user: { columns: { id: true, username: true, avatarUrl: true } } } } }, + user: { + columns: { id: true, username: true, avatarUrl: true }, + with: { wallets: { columns: { address: true, isPrimary: true } } }, }, }, + })) as ConversationMemberPayload[]; + + res.json({ members: members.map(serializeConversationMember) }); +}); + +conversationsRouter.post('/:id/members', async (req: AuthRequest, res) => { + const requesterId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + const newUserId = typeof req.body.userId === 'string' ? req.body.userId : undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + if (!newUserId) { + res.status(400).json({ error: 'userId is required' }); + return; + } + + const conversation = await db.query.conversations.findFirst({ + where: eq(conversations.id, conversationId), + columns: { id: true, type: true }, }); - const result = memberships.map((m) => m.conversation); - res.json(result); + if (!conversation) { + res.status(404).json({ error: 'Conversation not found' }); + return; + } + + if (conversation.type === 'dm') { + res.status(400).json({ error: 'DM conversations cannot add members' }); + return; + } + + const requesterMembership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, requesterId), + ), + }); + + if (!requesterMembership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + const existingMembership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, newUserId), + ), + }); + + if (existingMembership) { + res.status(409).json({ error: 'User is already a member' }); + return; + } + + try { + const [newMembership] = await db + .insert(conversationMembers) + .values({ conversationId, userId: newUserId }) + .returning(); + + if (!newMembership) { + res.status(500).json({ error: 'Failed to add conversation member' }); + return; + } + + const members = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, conversationId), + columns: { userId: true }, + }); + + await invalidateConversationCaches(members.map((member) => member.userId)); + + getSocketServer()?.to(conversationId).emit('member_joined', { + userId: newUserId, + conversationId, + }); + + res.status(201).json({ + id: newMembership.id, + conversationId: newMembership.conversationId, + userId: newMembership.userId, + joinedAt: newMembership.joinedAt, + }); + } catch { + res.status(409).json({ error: 'Database conflict or validation error' }); + } +}); + +// PATCH /conversations/:id — Update group conversation name/avatar. Only members can update. +conversationsRouter.patch('/:id', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + const { name, avatarUrl } = req.body as { name?: string; avatarUrl?: string }; + + if (name === undefined && avatarUrl === undefined) { + res.status(400).json({ error: 'At least one of name or avatarUrl must be provided' }); + return; + } + + if (name !== undefined && typeof name !== 'string') { + res.status(400).json({ error: 'name must be a string' }); + return; + } + + if (avatarUrl !== undefined && typeof avatarUrl !== 'string') { + res.status(400).json({ error: 'avatarUrl must be a string' }); + return; + } + + const conversation = await db.query.conversations.findFirst({ + where: eq(conversations.id, conversationId), + columns: { id: true, type: true }, + }); + + if (!conversation) { + res.status(404).json({ error: 'Conversation not found' }); + return; + } + + if (conversation.type === 'dm') { + res.status(400).json({ error: 'DM conversations cannot be updated' }); + return; + } + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + const updateData: { name?: string; avatarUrl?: string } = {}; + if (name !== undefined) updateData.name = name; + if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl; + + try { + const [updated] = await db + .update(conversations) + .set(updateData) + .where(eq(conversations.id, conversationId)) + .returning(); + + if (!updated) { + res.status(500).json({ error: 'Failed to update conversation' }); + return; + } + + const members = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, conversationId), + columns: { userId: true }, + }); + + await invalidateConversationCaches(members.map((member) => member.userId)); + + getSocketServer()?.to(conversationId).emit('conversation_updated', { + id: updated.id, + type: updated.type, + name: updated.name, + avatarUrl: updated.avatarUrl, + createdAt: updated.createdAt, + }); + + res.json(updated); + } catch { + res.status(500).json({ error: 'Failed to update conversation' }); + } +}); + +// #14 — GET /conversations/:id/messages +// Cursor-based pagination via ?before=&limit= (max 50). +// Returns messages in ascending order with a `nextCursor` field. +conversationsRouter.get('/:id/messages', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + // Parse & clamp limit + const rawLimit = parseInt(req.query['limit'] as string, 10); + const limit = + Number.isFinite(rawLimit) && rawLimit > 0 + ? Math.min(rawLimit, MAX_MESSAGES_LIMIT) + : DEFAULT_MESSAGES_LIMIT; + + const before = typeof req.query['before'] === 'string' ? req.query['before'] : undefined; + + // Membership check — non-members receive 403 + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + // Resolve cursor: look up the `createdAt` of the "before" message + let cursor: Date | undefined; + if (before) { + const ref = await db.query.messages.findFirst({ + where: eq(messages.id, before), + }); + if (!ref) { + res.status(400).json({ error: 'Invalid cursor' }); + return; + } + cursor = ref.createdAt; + } + + // Fetch one extra to determine whether there is a next page + const rows = await db.query.messages.findMany({ + where: cursor + ? and(eq(messages.conversationId, conversationId), lt(messages.createdAt, cursor)) + : eq(messages.conversationId, conversationId), + orderBy: desc(messages.createdAt), + limit: limit + 1, + with: { sender: { columns: { id: true, username: true, avatarUrl: true } } }, + }); + + const hasMore = rows.length > limit; + const page = hasMore ? rows.slice(0, limit) : rows; + + // Return in ascending (oldest-first) order + page.reverse(); + + const nextCursor = hasMore ? (page[0]?.id ?? null) : null; + + res.json({ messages: page, nextCursor }); +}); + +conversationsRouter.get('/:id/search', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + const query = typeof req.query.q === 'string' ? req.query.q.trim() : ''; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + if (!query) { + res.status(400).json({ error: 'Search query is required' }); + return; + } + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + const results = await db.execute<{ + id: string; + conversationId: string; + senderId: string; + content: string; + createdAt: Date; + snippet: string; + rank: string; + }>(sql` + WITH search_query AS ( + SELECT websearch_to_tsquery('english', ${query}) AS query + ) + SELECT + ${messages.id} AS "id", + ${messages.conversationId} AS "conversationId", + ${messages.senderId} AS "senderId", + ${messages.content} AS "content", + ${messages.createdAt} AS "createdAt", + ts_headline( + 'english', + ${messages.content}, + search_query.query, + 'StartSel=, StopSel=, MaxWords=24, MinWords=8, ShortWord=3, HighlightAll=false' + ) AS "snippet", + ts_rank_cd(to_tsvector('english', ${messages.content}), search_query.query) AS "rank" + FROM ${messages}, search_query + WHERE ${messages.conversationId} = ${conversationId} + AND ${messages.deletedAt} IS NULL + AND search_query.query @@ to_tsvector('english', ${messages.content}) + ORDER BY "rank" DESC, ${messages.createdAt} DESC + LIMIT ${SEARCH_RESULT_LIMIT} + `); + + res.json({ results }); +}); + +// PATCH /conversations/:id/settings — update muted/archived state for the authenticated user +conversationsRouter.patch('/:id/settings', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + const { muted, archived } = req.body as { muted?: boolean; archived?: boolean }; + + if (muted === undefined && archived === undefined) { + res.status(400).json({ error: 'At least one of muted or archived is required' }); + return; + } + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + const updates: Partial<{ isMuted: boolean; isArchived: boolean }> = {}; + if (muted !== undefined) updates.isMuted = muted; + if (archived !== undefined) updates.isArchived = archived; + + const [updated] = await db + .update(conversationMembers) + .set(updates) + .where( + and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + ) + .returning(); + + // Invalidate conversation list cache for this user + if (redis) { + try { + await redis.del(convCacheKey(userId)); + } catch { + // Ignore + } + } + + res.json({ isMuted: updated!.isMuted, isArchived: updated!.isArchived }); +}); + +// Save a token transfer for a conversation +conversationsRouter.post('/:id/transfers', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + // Check membership + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + const recipientAddress = req.body.recipient_address ?? req.body.recipientAddress; + const amount = req.body.amount; + const tokenContractId = req.body.token_contract_id ?? req.body.tokenContractId; + const txHash = req.body.tx_hash ?? req.body.txHash; + const memo = req.body.memo; + + if (!recipientAddress || amount === undefined || !tokenContractId || !txHash) { + res + .status(400) + .json({ error: 'recipientAddress, amount, tokenContractId, and txHash are required' }); + return; + } + + // Check for duplicate txHash + const existing = await db.query.tokenTransfers.findFirst({ + where: eq(tokenTransfers.txHash, txHash), + }); + + if (existing) { + res.status(409).json({ error: 'Transaction hash already exists' }); + return; + } + + try { + const [newTransfer] = await db + .insert(tokenTransfers) + .values({ + conversationId, + senderId: userId, + recipientAddress, + amount: String(amount), + tokenContractId, + txHash, + memo: memo ?? null, + }) + .returning(); + + res.status(201).json(newTransfer); + } catch { + res.status(409).json({ error: 'Database conflict or validation error' }); + } +}); + +// List token transfers for a conversation +conversationsRouter.get('/:id/transfers', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + // Check membership + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not a member of this conversation' }); + return; + } + + try { + const transfers = await db.query.tokenTransfers.findMany({ + where: eq(tokenTransfers.conversationId, conversationId), + orderBy: desc(tokenTransfers.createdAt), + }); + + res.json(transfers); + } catch { + res.status(500).json({ error: 'Failed to retrieve transfers' }); + } +}); + +conversationsRouter.delete('/:id/leave', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const conversationId = req.params['id'] as string | undefined; + + if (!conversationId) { + res.status(400).json({ error: 'Conversation id is required' }); + return; + } + + const conversation = await db.query.conversations.findFirst({ + where: eq(conversations.id, conversationId), + columns: { id: true, type: true }, + }); + + if (!conversation) { + res.status(404).json({ error: 'Conversation not found' }); + return; + } + + if (conversation.type === 'dm') { + res.status(400).json({ error: 'DM conversations cannot be left' }); + return; + } + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(404).json({ error: 'Conversation membership not found' }); + return; + } + + const members = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, conversationId), + columns: { userId: true }, + }); + + if (members.length === 1) { + await db.delete(conversations).where(eq(conversations.id, conversationId)); + } else { + await db + .delete(conversationMembers) + .where( + and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + ); + } + + await invalidateConversationCaches(members.map((member) => member.userId)); + + res.status(204).send(); }); diff --git a/apps/backend/src/routes/messages.ts b/apps/backend/src/routes/messages.ts new file mode 100644 index 0000000..0c3838b --- /dev/null +++ b/apps/backend/src/routes/messages.ts @@ -0,0 +1,55 @@ +import { Router } from 'express'; +import type { IRouter } from 'express'; +import { and, eq } from 'drizzle-orm'; +import { db } from '../db/index.js'; +import { conversationMembers, messages } from '../db/schema.js'; +import { requireAuth, type AuthRequest } from '../middleware/auth.js'; +import { invalidateConversationCaches } from '../lib/conversationCache.js'; +import { getSocketServer } from '../lib/socket.js'; + +export const messagesRouter: IRouter = Router(); + +messagesRouter.use(requireAuth); + +messagesRouter.delete('/:id', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const messageId = req.params['id'] as string | undefined; + + if (!messageId) { + res.status(400).json({ error: 'Message id is required' }); + return; + } + + const message = await db.query.messages.findFirst({ + where: eq(messages.id, messageId), + }); + + if (!message) { + res.status(404).json({ error: 'Message not found' }); + return; + } + + if (message.senderId !== userId) { + res.status(403).json({ error: 'You can only delete your own messages' }); + return; + } + + await db + .update(messages) + .set({ deletedAt: new Date() }) + .where(and(eq(messages.id, messageId), eq(messages.senderId, userId))); + + getSocketServer()?.to(message.conversationId).emit('message_deleted', { + messageId: message.id, + conversationId: message.conversationId, + }); + + const members = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, message.conversationId), + columns: { userId: true }, + }); + + await invalidateConversationCaches(members.map((member) => member.userId)); + + res.status(204).send(); +}); diff --git a/apps/backend/src/routes/users.ts b/apps/backend/src/routes/users.ts new file mode 100644 index 0000000..3883079 --- /dev/null +++ b/apps/backend/src/routes/users.ts @@ -0,0 +1,228 @@ +import { Router, type Router as RouterType } from 'express'; +import { eq, and, or, ilike, exists, sql } from 'drizzle-orm'; +import { db } from '../db/index.js'; +import { users, wallets } from '../db/schema.js'; +import { requireAuth, type AuthRequest } from '../middleware/auth.js'; +import { redis } from '../lib/redis.js'; +import { isOnline } from '../services/presence.js'; +import { fetchAndConsumeKeyBundle } from '../services/keyBundle.js'; + +export const usersRouter: RouterType = Router(); + +usersRouter.use(requireAuth); + +usersRouter.get('/search', async (req: AuthRequest, res) => { + const raw = req.query['q']; + const q = typeof raw === 'string' ? raw.trim() : ''; + + if (!q) { + res.status(400).json({ error: 'Query parameter "q" is required' }); + return; + } + + // Escape LIKE wildcards so user input is treated literally in the prefix match. + const prefix = `${q.replace(/[\\%_]/g, '\\$&')}%`; + + try { + const results = await db.query.users.findMany({ + where: or( + ilike(users.username, prefix), + exists( + db + .select({ one: sql`1` }) + .from(wallets) + .where(and(eq(wallets.userId, users.id), eq(wallets.address, q))), + ), + ), + columns: { + id: true, + username: true, + avatarUrl: true, + }, + with: { + wallets: { + columns: { address: true, isPrimary: true }, + }, + }, + limit: 10, + }); + + res.json( + results.map((user) => ({ + id: user.id, + username: user.username, + avatarUrl: user.avatarUrl, + primaryWalletAddress: user.wallets.find((w) => w.isPrimary)?.address ?? null, + })), + ); + } catch { + res.status(500).json({ error: 'Search failed' }); + } +}); + +usersRouter.get('/me', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + + try { + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + columns: { + id: true, + username: true, + avatarUrl: true, + createdAt: true, + }, + with: { + wallets: { + columns: { + address: true, + isPrimary: true, + }, + }, + }, + }); + + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + + res.json({ + id: user.id, + username: user.username, + avatarUrl: user.avatarUrl, + wallets: user.wallets.map((w) => ({ + address: w.address, + isPrimary: w.isPrimary, + })), + createdAt: user.createdAt, + }); + } catch { + res.status(404).json({ error: 'User not found' }); + } +}); + +usersRouter.get('/:id', async (req: AuthRequest, res) => { + const id = req.params['id'] as string; + + try { + const user = await db.query.users.findFirst({ + where: eq(users.id, id), + columns: { + id: true, + username: true, + avatarUrl: true, + }, + with: { + wallets: { + columns: { + address: true, + isPrimary: true, + }, + }, + }, + }); + + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + + res.json({ + id: user.id, + username: user.username, + avatarUrl: user.avatarUrl, + wallets: user.wallets.map((w) => ({ + address: w.address, + isPrimary: w.isPrimary, + })), + }); + } catch { + res.status(404).json({ error: 'User not found' }); + } +}); + +// GET /users/:userId/devices/:deviceId/key-bundle — fetch + consume a prekey bundle. +// +// Returns the recipient device's public prekey bundle and atomically consumes one +// one-time prekey so no two senders are handed the same one. When the one-time +// pool is exhausted the bundle is returned with `oneTimePreKey: null`. Unknown or +// revoked devices return 404. Only public key material is ever returned. +usersRouter.get('/:userId/devices/:deviceId/key-bundle', async (req: AuthRequest, res) => { + const userId = req.params['userId'] as string | undefined; + const deviceId = req.params['deviceId'] as string | undefined; + + if (!userId || !deviceId) { + res.status(400).json({ error: 'userId and deviceId are required' }); + return; + } + + const result = await fetchAndConsumeKeyBundle(userId, deviceId); + + if (!result.ok) { + res.status(result.status).json({ error: result.error }); + return; + } + + res.json(result.bundle); +}); + +usersRouter.get('/:id/presence', async (req: AuthRequest, res) => { + const id = req.params['id'] as string; + if (!redis) { + res.json({ online: false }); + return; + } + const online = await isOnline(redis, id); + res.json({ online }); +}); + +usersRouter.patch('/me', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const { username, avatarUrl } = req.body; + + const updateData: Partial = {}; + + if (avatarUrl !== undefined) { + updateData.avatarUrl = avatarUrl; + } + + if (username !== undefined) { + if (typeof username !== 'string' || !/^[a-zA-Z0-9_]{3,30}$/.test(username)) { + res + .status(400) + .json({ error: 'Username must be 3-30 alphanumeric characters and underscores only' }); + return; + } + + // Check conflict + const existing = await db.query.users.findFirst({ + where: eq(users.username, username), + }); + if (existing && existing.id !== userId) { + res.status(409).json({ error: 'Username is already taken' }); + return; + } + + updateData.username = username; + } + + updateData.updatedAt = new Date(); + + try { + const [updatedUser] = await db + .update(users) + .set(updateData) + .where(eq(users.id, userId)) + .returning(); + + if (!updatedUser) { + res.status(404).json({ error: 'User not found' }); + return; + } + + res.json(updatedUser); + } catch { + res.status(409).json({ error: 'Username conflict or database error' }); + } +}); diff --git a/apps/backend/src/schemas/auth.schemas.ts b/apps/backend/src/schemas/auth.schemas.ts new file mode 100644 index 0000000..c22136b --- /dev/null +++ b/apps/backend/src/schemas/auth.schemas.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const ChallengeSchema = z.object({ + walletAddress: z.string().min(1, 'walletAddress is required'), +}); + +export const VerifySchema = z.object({ + walletAddress: z.string().min(1, 'walletAddress is required'), + signature: z.string().min(1, 'signature is required'), + nonce: z.string().min(1, 'nonce is required'), +}); + +export type ChallengeBody = z.infer; +export type VerifyBody = z.infer; diff --git a/apps/backend/src/services/keyBundle.ts b/apps/backend/src/services/keyBundle.ts new file mode 100644 index 0000000..df732cd --- /dev/null +++ b/apps/backend/src/services/keyBundle.ts @@ -0,0 +1,86 @@ +/** + * Prekey bundle fetch + one-time prekey consumption (#160). + * + * Builds the X3DH/Signal-style key bundle a sender needs to start an encrypted + * session with a recipient device. The single one-time prekey (OTP) in the + * bundle is *consumed* as it is handed out: it is claimed with one atomic + * `UPDATE ... WHERE consumed = false ... RETURNING` guarded by `FOR UPDATE SKIP + * LOCKED`, so two senders fetching concurrently can never receive the same OTP. + * When the pool is exhausted the bundle is still returned with `oneTimePreKey: + * null` — sessions can be established from the signed prekey alone. + * + * Only public key material and signatures are exposed; private keys never reach + * the server, so nothing private can ever be returned. + */ +import { and, eq, sql } from 'drizzle-orm'; +import { db } from '../db/index.js'; +import { devices, oneTimePreKeys } from '../db/schema.js'; + +export interface PreKeyBundle { + identityPublicKey: string; + registrationId: number; + signedPreKey: { keyId: number; publicKey: string; signature: string }; + oneTimePreKey: { keyId: number; publicKey: string } | null; +} + +export type KeyBundleResult = + | { ok: true; bundle: PreKeyBundle } + | { ok: false; status: 404; error: string }; + +/** + * Atomically claim the next unconsumed one-time prekey for a device. + * + * The whole select-and-mark is a single statement, so it is race-free under + * concurrent fetches: `FOR UPDATE SKIP LOCKED` makes parallel callers skip a row + * another transaction is already claiming rather than block on or re-read it. + * Returns `null` when no unconsumed prekey remains. + */ +async function consumeOneTimePreKey( + deviceId: string, +): Promise<{ keyId: number; publicKey: string } | null> { + const rows = await db.execute<{ keyId: number; publicKey: string }>(sql` + UPDATE ${oneTimePreKeys} + SET consumed = true + WHERE id = ( + SELECT id + FROM ${oneTimePreKeys} + WHERE device_id = ${deviceId} AND consumed = false + ORDER BY key_id + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING key_id AS "keyId", public_key AS "publicKey" + `); + + return rows[0] ?? null; +} + +export async function fetchAndConsumeKeyBundle( + userId: string, + deviceId: string, +): Promise { + const device = await db.query.devices.findFirst({ + where: and(eq(devices.id, deviceId), eq(devices.userId, userId)), + }); + + // Unknown or revoked devices are indistinguishable to callers — both 404. + if (!device || device.revokedAt) { + return { ok: false, status: 404, error: 'Device not found' }; + } + + const oneTimePreKey = await consumeOneTimePreKey(deviceId); + + return { + ok: true, + bundle: { + identityPublicKey: device.identityPublicKey, + registrationId: device.registrationId, + signedPreKey: { + keyId: device.signedPreKeyId, + publicKey: device.signedPreKeyPublic, + signature: device.signedPreKeySignature, + }, + oneTimePreKey, + }, + }; +} diff --git a/apps/backend/src/services/presence.ts b/apps/backend/src/services/presence.ts new file mode 100644 index 0000000..ccda9cd --- /dev/null +++ b/apps/backend/src/services/presence.ts @@ -0,0 +1,64 @@ +/** + * Online presence tracking (#13). + * + * Stores userId → socketId mapping in Redis with a 60-second TTL that is + * refreshed on every heartbeat. Uses a Redis set per userId to support + * multiple tabs/connections but counting as a single presence entry. + * + * - On connect: add socketId to `presence:{userId}` set, set TTL 60s + * - On heartbeat: refresh TTL to 60s + * - On disconnect: remove socketId from set, if set empty → user_offline + * - GET /users/:id/presence → { online: boolean } + */ +import type { Redis } from 'ioredis'; + +const PRESENCE_TTL = 60; // seconds + +function presenceKey(userId: string): string { + return `presence:${userId}`; +} + +/** + * Register a socket connection for a user. Adds the socketId to the + * user's presence set and sets/refreshes the TTL. + */ +export async function setOnline(redis: Redis, userId: string, socketId: string): Promise { + const key = presenceKey(userId); + await redis.sadd(key, socketId); + await redis.expire(key, PRESENCE_TTL); +} + +/** + * Refresh the presence TTL (called on heartbeat). + */ +export async function refreshPresence(redis: Redis, userId: string): Promise { + const key = presenceKey(userId); + const exists = await redis.exists(key); + if (exists) { + await redis.expire(key, PRESENCE_TTL); + } +} + +/** + * Remove a socket connection from the user's presence set. + * Returns true if the user has gone fully offline (no remaining sockets). + */ +export async function setOffline(redis: Redis, userId: string, socketId: string): Promise { + const key = presenceKey(userId); + await redis.srem(key, socketId); + const remaining = await redis.scard(key); + if (remaining === 0) { + await redis.del(key); + return true; + } + return false; +} + +/** + * Check if a user is currently online. + */ +export async function isOnline(redis: Redis, userId: string): Promise { + const key = presenceKey(userId); + const count = await redis.scard(key); + return count > 0; +} diff --git a/apps/backend/src/services/stellarListener.ts b/apps/backend/src/services/stellarListener.ts new file mode 100644 index 0000000..afefacf --- /dev/null +++ b/apps/backend/src/services/stellarListener.ts @@ -0,0 +1,269 @@ +/** + * Stellar event listener for `token_transfer` (#46). + * + * Subscribes to contract events emitted by the `token_transfer` Soroban + * contract and writes each into the `token_transfers` table. The listener: + * + * - Polls Soroban RPC `getEvents` on a short interval (cursor-based), + * which is the supported pattern in stellar-sdk's `rpc` module today. + * - Reconnects automatically after a transient failure with exponential + * backoff capped at 30 seconds. + * - Upserts on the unique `tx_hash` so a reconnect that re-reads a page + * of events produces no duplicates. + * - Logs errors via the standard backend logger but never rethrows out + * of `runForever`, so the API server stays up even if the chain is + * unreachable. + * + * The actual fetch is wrapped behind a `fetchEvents` dependency so the + * unit tests under `__tests__/stellarListener.test.ts` can drive the + * loop deterministically without hitting Soroban RPC. + */ +import { rpc } from '@stellar/stellar-sdk'; +import { db } from '../db/index.js'; +import { tokenTransfers, messages, conversations, users } from '../db/schema.js'; +import { eq, sql } from 'drizzle-orm'; + +const DEFAULT_POLL_INTERVAL_MS = 5_000; +const DEFAULT_BACKOFF_BASE_MS = 1_000; +const DEFAULT_BACKOFF_MAX_MS = 30_000; + +export interface StellarTransferEvent { + /** Soroban tx hash that produced the event. */ + txHash: string; + /** Ledger sequence the event was included in. */ + ledger: number; + /** Stellar address that authorised the transfer. */ + from: string; + /** Stellar address that received the transfer. */ + to: string; + /** Amount in token units (i128 as decimal string). */ + amount: string; + /** Raw memo bytes hex-encoded (matches the contract's emitted memo). */ + memoHex?: string; + /** Cursor token the next `fetchEvents` call should resume from. */ + cursor: string; +} + +export interface StellarListenerDeps { + /** Optional logger; defaults to a console wrapper. */ + log?: { + info: (msg: string, ctx?: unknown) => void; + warn: (msg: string, ctx?: unknown) => void; + error: (msg: string, ctx?: unknown) => void; + }; + /** Fetches the next page of events starting at `cursor`. Must throw on + * network / RPC failures so the listener can apply backoff. */ + fetchEvents: (cursor: string | null) => Promise; + /** Persistence layer; swapped out in tests. */ + persistEvent?: (event: StellarTransferEvent) => Promise; + /** Pause between successful polls (default 5s). */ + pollIntervalMs?: number; + /** Initial backoff after a failure (doubles up to `backoffMaxMs`). */ + backoffBaseMs?: number; + backoffMaxMs?: number; + /** Abort signal that breaks out of `runForever`. */ + signal?: AbortSignal; +} + +const consoleLogger = { + info: (msg: string, ctx?: unknown) => console.log(`[stellar-listener] ${msg}`, ctx ?? ''), + warn: (msg: string, ctx?: unknown) => console.warn(`[stellar-listener] ${msg}`, ctx ?? ''), + error: (msg: string, ctx?: unknown) => console.error(`[stellar-listener] ${msg}`, ctx ?? ''), +}; + +/** + * Default persistence: upsert on `txHash`, attempting to associate the + * transfer with a message whose id matches the decoded memo bytes (if any). + */ +async function defaultPersistEvent(event: StellarTransferEvent): Promise { + let conversationId: string | null = null; + let senderId: string | null = null; + + if (event.memoHex) { + try { + const memo = Buffer.from(event.memoHex, 'hex').toString('utf-8').trim(); + // The contract emits a message UUID in the memo when the transfer + // originated from a chat message; non-UUID memos are ignored. + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(memo)) { + const [existing] = await db + .select({ + id: messages.id, + conversationId: messages.conversationId, + senderId: messages.senderId, + }) + .from(messages) + .where(eq(messages.id, memo)) + .limit(1); + if (existing) { + conversationId = existing.conversationId; + senderId = existing.senderId; + } + } + } catch { + // Non-fatal — memo just stays raw, no association. + } + } + + // Fallbacks if not found (required columns in tokenTransfers) + if (!conversationId || !senderId) { + const [fallbackConv] = await db.select({ id: conversations.id }).from(conversations).limit(1); + const [fallbackUser] = await db.select({ id: users.id }).from(users).limit(1); + if (!fallbackConv || !fallbackUser) { + return; + } + conversationId = fallbackConv.id; + senderId = fallbackUser.id; + } + + await db + .insert(tokenTransfers) + .values({ + txHash: event.txHash, + conversationId, + senderId, + recipientAddress: event.to, + amount: event.amount, + tokenContractId: 'placeholder_token_contract_id', + memo: event.memoHex ?? null, + }) + .onConflictDoUpdate({ + target: tokenTransfers.txHash, + set: { + createdAt: sql`now()`, + }, + }); +} + +/** + * Run the listener loop until `signal` aborts (or process exit). Never + * throws — RPC / DB errors are logged and the loop backs off. + */ +export async function runForever(deps: StellarListenerDeps): Promise { + const log = deps.log ?? consoleLogger; + const persist = deps.persistEvent ?? defaultPersistEvent; + const pollMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; + const backoffBase = deps.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS; + const backoffMax = deps.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS; + + let cursor: string | null = null; + let consecutiveFailures = 0; + + log.info('listener starting'); + + while (!deps.signal?.aborted) { + try { + const events = await deps.fetchEvents(cursor); + consecutiveFailures = 0; + + for (const event of events) { + try { + await persist(event); + cursor = event.cursor; + } catch (err) { + // Per-event failure: log and move on so one bad row doesn't + // freeze the cursor. cursor is NOT advanced here so the next + // poll retries. + log.warn('failed to persist event', { + txHash: event.txHash, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + await wait(pollMs, deps.signal); + } catch (err) { + consecutiveFailures += 1; + const delay = Math.min(backoffBase * Math.pow(2, consecutiveFailures - 1), backoffMax); + log.error('fetch failed; reconnecting after backoff', { + attempt: consecutiveFailures, + delayMs: delay, + error: err instanceof Error ? err.message : String(err), + }); + await wait(delay, deps.signal); + } + } + + log.info('listener stopped (signal aborted)'); +} + +function wait(ms: number, signal?: AbortSignal): Promise { + if (signal?.aborted) return Promise.resolve(); + return new Promise((resolve) => { + const timer = setTimeout(resolve, ms); + signal?.addEventListener( + 'abort', + () => { + clearTimeout(timer); + resolve(); + }, + { once: true }, + ); + }); +} + +// ── Production wiring ──────────────────────────────────────────────────────── + +/** + * Build a default fetcher that talks to a Soroban RPC server and filters + * events by the configured `token_transfer` contract id. Returns a thunk + * suitable for passing into `runForever({ fetchEvents })`. + */ +export function buildRpcFetcher(opts: { + rpcUrl: string; + contractId: string; + pageSize?: number; +}): StellarListenerDeps['fetchEvents'] { + const server = new rpc.Server(opts.rpcUrl, { allowHttp: opts.rpcUrl.startsWith('http://') }); + const pageSize = opts.pageSize ?? 100; + type RpcEvent = { + txHash?: string; + ledger?: number; + value?: { from?: string; to?: string; amount?: string | number; memo?: string }; + pagingToken?: string; + }; + const eventServer = server as unknown as { + getEvents: (request: { + startLedger: undefined; + cursor: string | undefined; + filters: Array<{ type: 'contract'; contractIds: string[]; topics: string[][] }>; + limit: number; + }) => Promise<{ events?: RpcEvent[] }>; + }; + + return async (cursor) => { + const startLedger = cursor ? undefined : undefined; // resume on cursor only + const response = await eventServer.getEvents({ + startLedger, + cursor: cursor ?? undefined, + filters: [ + { + type: 'contract', + contractIds: [opts.contractId], + topics: [['transfer']], + }, + ], + limit: pageSize, + }); + + const events = response.events ?? []; + + return events + .filter((e) => e.txHash && e.value?.from && e.value?.to && e.value?.amount != null) + .map((e) => { + const event: StellarTransferEvent = { + txHash: e.txHash as string, + ledger: e.ledger ?? 0, + from: e.value!.from as string, + to: e.value!.to as string, + amount: String(e.value!.amount), + cursor: e.pagingToken ?? '', + }; + + if (e.value?.memo !== undefined) { + event.memoHex = e.value.memo; + } + + return event; + }); + }; +} diff --git a/apps/backend/src/socket/messaging.ts b/apps/backend/src/socket/messaging.ts index 71a494c..17d3bab 100644 --- a/apps/backend/src/socket/messaging.ts +++ b/apps/backend/src/socket/messaging.ts @@ -1,12 +1,11 @@ import type { Server } from 'socket.io'; -import { and, eq, lt, desc } from 'drizzle-orm'; +import { and, eq, lt, desc, sql } from 'drizzle-orm'; import { db } from '../db/index.js'; -import { - conversations, - conversationMembers, - messages, -} from '../db/schema.js'; +import { conversations, conversationMembers, messages } from '../db/schema.js'; import type { AuthSocket } from '../middleware/socketAuth.js'; +import { invalidateConversationCaches } from '../lib/conversationCache.js'; +import { serializeMessage } from '../lib/messages.js'; +import { redis } from '../lib/redis.js'; const PAGE_SIZE = 30; @@ -64,15 +63,66 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void .returning(); io.to(conversationId).emit('new_message', message); + + const members = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, conversationId), + columns: { userId: true }, + }); + + await invalidateConversationCaches(members.map((member) => member.userId)); }); // ── message_history ──────────────────────────────────────────────────────── // Payload: { conversationId: string; before?: string } (before = message id cursor) // Returns the last PAGE_SIZE messages, optionally before a cursor for pagination. + socket.on('message_history', async (payload: { conversationId: string; before?: string }) => { + const { conversationId, before } = payload; + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + socket.emit('error', { + event: 'message_history', + message: 'Not a member of this conversation', + }); + return; + } + + let cursor: Date | undefined; + if (before) { + const ref = await db.query.messages.findFirst({ + where: eq(messages.id, before), + }); + cursor = ref?.createdAt; + } + + const history = await db.query.messages.findMany({ + where: cursor + ? and(eq(messages.conversationId, conversationId), lt(messages.createdAt, cursor)) + : eq(messages.conversationId, conversationId), + orderBy: desc(messages.createdAt), + limit: PAGE_SIZE, + with: { sender: { columns: { id: true, username: true, avatarUrl: true } } }, + }); + + socket.emit('message_history', { + conversationId, + messages: history.reverse().map((message) => serializeMessage(message)), + }); + }); + + // ── message_read ─────────────────────────────────────────────────────────── + // Payload: { conversationId: string; lastReadMessageId: string } + // Persists the caller's read position and broadcasts to the room. socket.on( - 'message_history', - async (payload: { conversationId: string; before?: string }) => { - const { conversationId, before } = payload; + 'message_read', + async (payload: { conversationId: string; lastReadMessageId: string }) => { + const { conversationId, lastReadMessageId } = payload; const membership = await db.query.conversationMembers.findFirst({ where: and( @@ -82,28 +132,37 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void }); if (!membership) { - socket.emit('error', { event: 'message_history', message: 'Not a member of this conversation' }); + socket.emit('error', { + event: 'message_read', + message: 'Not a member of this conversation', + }); return; } - let cursor: Date | undefined; - if (before) { - const ref = await db.query.messages.findFirst({ - where: eq(messages.id, before), + // Ensure message exists in this conversation (prevents spoofed reads) + const message = await db.query.messages.findFirst({ + where: and(eq(messages.id, lastReadMessageId), eq(messages.conversationId, conversationId)), + }); + + if (!message) { + socket.emit('error', { + event: 'message_read', + message: 'Message not found in conversation', }); - cursor = ref?.createdAt; + return; } - const history = await db.query.messages.findMany({ - where: cursor - ? and(eq(messages.conversationId, conversationId), lt(messages.createdAt, cursor)) - : eq(messages.conversationId, conversationId), - orderBy: desc(messages.createdAt), - limit: PAGE_SIZE, - with: { sender: { columns: { id: true, username: true, avatarUrl: true } } }, - }); + await db + .update(conversationMembers) + .set({ lastReadMessageId }) + .where( + and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + ); - socket.emit('message_history', { conversationId, messages: history.reverse() }); + io.to(conversationId).emit('read_receipt', { userId, lastReadMessageId }); }, ); @@ -117,21 +176,162 @@ export function registerMessagingHandlers(io: Server, socket: AuthSocket): void const allMembers = Array.from(new Set([userId, ...memberIds])); - const [conversation] = await db - .insert(conversations) - .values({ type, name }) - .returning(); + const [conversation] = await db.insert(conversations).values({ type, name }).returning(); if (!conversation) { - socket.emit('error', { event: 'create_conversation', message: 'Failed to create conversation' }); + socket.emit('error', { + event: 'create_conversation', + message: 'Failed to create conversation', + }); return; } - await db.insert(conversationMembers).values( - allMembers.map((uid) => ({ conversationId: conversation.id, userId: uid })), - ); + await db + .insert(conversationMembers) + .values(allMembers.map((uid) => ({ conversationId: conversation.id, userId: uid }))); socket.emit('conversation_created', conversation); + + await invalidateConversationCaches(allMembers); }, ); + // ── typing_start ──────────────────────────────────────────────────────────── + // Payload: { conversationId: string } + // Broadcasts to the room excluding the sender. No DB write. + socket.on('typing_start', async (payload: { conversationId: string }) => { + const { conversationId } = payload; + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + socket.emit('error', { event: 'typing_start', message: 'Not a member of this conversation' }); + return; + } + + socket.to(conversationId).emit('typing_start', { conversationId, userId }); + }); + + // ── typing_stop ───────────────────────────────────────────────────────────── + // Payload: { conversationId: string } + // Broadcasts to the room excluding the sender. No DB write. + socket.on('typing_stop', async (payload: { conversationId: string }) => { + const { conversationId } = payload; + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + socket.emit('error', { event: 'typing_stop', message: 'Not a member of this conversation' }); + return; + } + + socket.to(conversationId).emit('typing_stop', { conversationId, userId }); + }); + + // ── ask_assistant ────────────────────────────────────────────────────────── + // Payload: { conversationId: string; content: string } + // Forwards to AI agent and posts reply from reserved assistant user. + // Rate-limit: 5 requests per user per minute. + const ASSISTANT_USER_ID = '00000000-0000-4000-8000-000000000000'; + + socket.on('ask_assistant', async (payload: { conversationId: string; content: string }) => { + const { conversationId, content } = payload; + + if (!content?.trim().startsWith('@assistant')) { + return; + } + + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + socket.emit('error', { + event: 'ask_assistant', + message: 'Not a member of this conversation', + }); + return; + } + + // Rate limiting + if (redis) { + const rlKey = `rl:ask_assistant:${userId}`; + const count = await redis.incr(rlKey); + if (count === 1) { + await redis.expire(rlKey, 60); + } + if (count > 5) { + socket.emit('error', { event: 'rate_limited', message: 'Rate limit exceeded' }); + return; + } + } + + // Forward to AI agent + try { + const response = await fetch('http://localhost:8000/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message: content, + conversation_id: conversationId, + }), + }); + + if (!response.ok) { + throw new Error('AI agent error'); + } + + const data = (await response.json()) as { reply: string }; + + // Ensure assistant user exists (upsert) + // Usually done via migration, but we can safely do it here or assume it exists. + // To be safe, we'll try to insert it and ignore conflict. + await db.execute(sql` + INSERT INTO users (id, username, avatar_url) + VALUES (${ASSISTANT_USER_ID}, 'Assistant', 'https://ui-avatars.com/api/?name=AI&background=0D8ABC&color=fff') + ON CONFLICT (id) DO NOTHING + `); + + // Add to conversation members if not already + await db.execute(sql` + INSERT INTO conversation_members (conversation_id, user_id) + VALUES (${conversationId}, ${ASSISTANT_USER_ID}) + ON CONFLICT DO NOTHING + `); + + // Post the reply + const [replyMessage] = await db + .insert(messages) + .values({ + conversationId, + senderId: ASSISTANT_USER_ID, + content: data.reply, + }) + .returning(); + + io.to(conversationId).emit('new_message', replyMessage); + + const members = await db.query.conversationMembers.findMany({ + where: eq(conversationMembers.conversationId, conversationId), + columns: { userId: true }, + }); + + await invalidateConversationCaches(members.map((member) => member.userId)); + } catch (err) { + console.error('ask_assistant error:', err); + socket.emit('error', { event: 'ask_assistant', message: 'Failed to get AI reply' }); + } + }); } diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts new file mode 100644 index 0000000..08b6a6b --- /dev/null +++ b/apps/backend/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + setupFiles: ['./src/__tests__/setup.ts'], + }, +}); diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json new file mode 100644 index 0000000..d85f5f7 --- /dev/null +++ b/apps/web/package-lock.json @@ -0,0 +1,7393 @@ +{ + "name": "web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.1.0", + "dependencies": { + "@stellar/freighter-api": "^6.0.1", + "framer-motion": "^12.41.0", + "lucide-react": "^1.21.0", + "next": "16.2.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "socket.io-client": "^4.5.0", + "stellar-sdk": "^11.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.0", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@next/env": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.0.tgz", + "integrity": "sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.0.tgz", + "integrity": "sha512-3D3pEMcGKfENC9Pzlkr67GOm+205+5hRdYPZvHuNIy5sr9k0ybSU8g+sxOO/R/RLEh/gWZ3UlY+5LmEyZ1xgXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.0.tgz", + "integrity": "sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.0.tgz", + "integrity": "sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.0.tgz", + "integrity": "sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.0.tgz", + "integrity": "sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.0.tgz", + "integrity": "sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.0.tgz", + "integrity": "sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.0.tgz", + "integrity": "sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.0.tgz", + "integrity": "sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "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/@stellar/freighter-api": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.1.tgz", + "integrity": "sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "6.0.3", + "semver": "7.7.1" + } + }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.1.0.tgz", + "integrity": "sha512-nMg7QSpFqCZFq3Je/lG12+DY18y01QHRNyCxvjM8i4myS9tPRMDq7zqGcd215BGbCJxenckiOW45YJjQjzdcMQ==", + "deprecated": "This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support.", + "license": "Apache-2.0", + "dependencies": { + "@stellar/js-xdr": "^3.1.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.1.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.1.tgz", + "integrity": "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "postcss": "8.5.15", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", + "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/type-utils": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.62.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", + "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", + "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.0", + "@typescript-eslint/types": "^8.62.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", + "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", + "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", + "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", + "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", + "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.0", + "@typescript-eslint/tsconfig-utils": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", + "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", + "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-addon-resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", + "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.2.tgz", + "integrity": "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-semver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz", + "integrity": "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.377", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.377.tgz", + "integrity": "sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "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.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", + "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.1.tgz", + "integrity": "sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.0.tgz", + "integrity": "sha512-LlVJrWnjIkgQRECjIOELyAtrWFqzn326ARS5ap7swc1YKL4wkry6/gszn6wi5ZDWKxKe7fanxArvhqMoAzbL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.13.0.tgz", + "integrity": "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "12.41.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.41.0.tgz", + "integrity": "sha512-OHAMNiCEON1RDBlRGuulsN5AD8ptMjvk5QWfFmYmBLPZ3zFGIJe60kQucQQf4cez1OzQmjYBWDY+dYfISkUdqg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.41.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/motion-dom": { + "version": "12.41.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.41.0.tgz", + "integrity": "sha512-Lk3J39fOGg6xNr1KRZsN6usDyBf8aP7MEbUPez1VCughHt79OrP7VGqNrPyFL0riaT7WS8t9DRw1M3BHtM/xKw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz", + "integrity": "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.0", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.0", + "@next/swc-darwin-x64": "16.2.0", + "@next/swc-linux-arm64-gnu": "16.2.0", + "@next/swc-linux-arm64-musl": "16.2.0", + "@next/swc-linux-x64-gnu": "16.2.0", + "@next/swc-linux-x64-musl": "16.2.0", + "@next/swc-win32-arm64-msvc": "16.2.0", + "@next/swc-win32-x64-msvc": "16.2.0", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "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" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-addon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", + "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-addon-resolve": "^1.3.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "require-addon": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stellar-sdk": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/stellar-sdk/-/stellar-sdk-11.3.0.tgz", + "integrity": "sha512-xOp2zpQm5TIbgJi7wJhAmJh+Uy0ew5GbGtj1kZv6HEWHgSvW95xYMxGaw6MWM9r2YPUSQySboE6JwDc9jdx53A==", + "deprecated": "⚠️ This package has moved to @stellar/stellar-sdk! 🚚", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^11.0.1", + "axios": "^1.6.8", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", + "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/apps/web/package.json b/apps/web/package.json index 77118a9..2e059c8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,12 +6,18 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "lint:fix": "eslint --fix" }, "dependencies": { + "@stellar/freighter-api": "^6.0.1", + "framer-motion": "^12.41.0", + "lucide-react": "^1.21.0", "next": "16.2.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "socket.io-client": "^4.5.0", + "stellar-sdk": "^11.0.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 6e416bd..04ba22c 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@stellar/freighter-api': + specifier: ^6.0.1 + version: 6.0.1 next: specifier: 16.2.0 version: 16.2.0(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -17,6 +20,12 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + socket.io-client: + specifier: ^4.5.0 + version: 4.8.3 + stellar-sdk: + specifier: ^11.0.0 + version: 11.3.0 devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -408,6 +417,18 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@stellar/freighter-api@6.0.1': + resolution: {integrity: sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==} + + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} + + '@stellar/stellar-base@11.0.1': + resolution: {integrity: sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -686,6 +707,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} @@ -739,6 +764,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -747,6 +775,9 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} + axios@1.17.0: + resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -758,11 +789,40 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-addon-resolve@1.10.0: + resolution: {integrity: sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==} + peerDependencies: + bare-url: '*' + peerDependenciesMeta: + bare-url: + optional: true + + bare-module-resolve@1.12.2: + resolution: {integrity: sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==} + peerDependencies: + bare-url: '*' + peerDependenciesMeta: + bare-url: + optional: true + + bare-semver@1.0.3: + resolution: {integrity: sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==} + + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.8: resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} engines: {node: '>=6.0.0'} hasBin: true + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -779,6 +839,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -812,6 +875,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -868,6 +935,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -886,6 +957,13 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + engine.io-client@6.6.5: + resolution: {integrity: sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + enhanced-resolve@5.20.1: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} @@ -1050,6 +1128,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1094,10 +1176,23 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1191,6 +1286,13 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1207,6 +1309,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -1475,6 +1580,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -1622,6 +1735,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1629,6 +1746,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -1649,6 +1769,10 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-addon@1.2.0: + resolution: {integrity: sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==} + engines: {bare: '>=1.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1677,6 +1801,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -1692,6 +1819,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1709,6 +1841,11 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1737,6 +1874,17 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + engines: {node: '>=10.0.0'} + + sodium-native@4.3.3: + resolution: {integrity: sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1744,6 +1892,10 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stellar-sdk@11.3.0: + resolution: {integrity: sha512-xOp2zpQm5TIbgJi7wJhAmJh+Uy0ew5GbGtj1kZv6HEWHgSvW95xYMxGaw6MWM9r2YPUSQySboE6JwDc9jdx53A==} + deprecated: ⚠️ This package has moved to @stellar/stellar-sdk! 🚚 + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -1811,10 +1963,17 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -1827,6 +1986,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1878,6 +2040,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -1903,6 +2068,22 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2265,6 +2446,28 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@socket.io/component-emitter@3.1.2': {} + + '@stellar/freighter-api@6.0.1': + dependencies: + buffer: 6.0.3 + semver: 7.7.1 + + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@11.0.1': + dependencies: + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + tweetnacl: 1.0.3 + optionalDependencies: + sodium-native: 4.3.3 + transitivePeerDependencies: + - bare-url + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2517,6 +2720,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -2603,20 +2812,52 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.11.1: {} + axios@1.17.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + axobject-query@4.1.0: {} balanced-match@1.0.2: {} balanced-match@4.0.4: {} + bare-addon-resolve@1.10.0: + dependencies: + bare-module-resolve: 1.12.2 + bare-semver: 1.0.3 + optional: true + + bare-module-resolve@1.12.2: + dependencies: + bare-semver: 1.0.3 + optional: true + + bare-semver@1.0.3: + optional: true + + base32.js@0.1.0: {} + + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.8: {} + bignumber.js@9.3.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2638,6 +2879,11 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2672,6 +2918,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -2726,6 +2976,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + detect-libc@2.1.2: {} doctrine@2.1.0: @@ -2742,6 +2994,20 @@ snapshots: emoji-regex@9.2.2: {} + engine.io-client@6.6.5: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.20.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 @@ -3058,6 +3324,8 @@ snapshots: esutils@2.0.3: {} + eventsource@2.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3100,10 +3368,20 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3198,6 +3476,15 @@ snapshots: dependencies: hermes-estree: 0.25.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3209,6 +3496,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -3460,6 +3749,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 @@ -3614,10 +3909,16 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -3647,6 +3948,13 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-addon@1.2.0: + dependencies: + bare-addon-resolve: 1.10.0 + transitivePeerDependencies: + - bare-url + optional: true + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3680,6 +3988,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -3695,6 +4005,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.1: {} + semver@7.7.4: {} set-function-length@1.2.2: @@ -3719,6 +4031,12 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -3785,10 +4103,49 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-client: 6.6.5 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.6: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + sodium-native@4.3.3: + dependencies: + require-addon: 1.2.0 + transitivePeerDependencies: + - bare-url + optional: true + source-map-js@1.2.1: {} stable-hash@0.0.5: {} + stellar-sdk@11.3.0: + dependencies: + '@stellar/stellar-base': 11.0.1 + axios: 1.17.0 + bignumber.js: 9.3.1 + eventsource: 2.0.2 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - bare-url + - debug + - supports-color + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -3870,10 +4227,18 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + toml@3.0.0: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -3887,6 +4252,8 @@ snapshots: tslib@2.8.1: {} + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3980,6 +4347,8 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -4027,6 +4396,10 @@ snapshots: word-wrap@1.2.5: {} + ws@8.20.1: {} + + xmlhttprequest-ssl@2.1.2: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/apps/web/src/app/app/conversations/[id]/page.tsx b/apps/web/src/app/app/conversations/[id]/page.tsx new file mode 100644 index 0000000..a407835 --- /dev/null +++ b/apps/web/src/app/app/conversations/[id]/page.tsx @@ -0,0 +1,13 @@ +export default async function ConversationPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( +
+ Conversation {id} +
+ ); +} diff --git a/apps/web/src/app/app/layout.tsx b/apps/web/src/app/app/layout.tsx new file mode 100644 index 0000000..6754a1c --- /dev/null +++ b/apps/web/src/app/app/layout.tsx @@ -0,0 +1,260 @@ +"use client"; + +import React, { useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useWallet } from "@/contexts/WalletContext"; + +// Custom premium SVG Icons to avoid dependency weight +const LogoIcon = () => ( + + + + +); + +const MessagesIcon = () => ( + + + +); + +const TreasuryIcon = () => ( + + + + + +); + +const ProposalsIcon = () => ( + + + + +); + +const WalletIcon = () => ( + + + +); + +interface NavItemProps { + href: string; + label: string; + icon: React.ReactNode; + active: boolean; +} + +const NavItem: React.FC = ({ href, label, icon, active }) => { + return ( + + {active && ( + + )} +
+ {icon} +
+ + {label} + + + ); +}; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const { publicKey, connect, disconnect } = useWallet(); + const [isConnecting, setIsConnecting] = useState(false); + + const handleWalletAction = async () => { + if (publicKey) { + disconnect(); + } else { + setIsConnecting(true); + try { + await connect(); + } catch (err) { + console.error("Wallet connection failed:", err); + } finally { + setIsConnecting(false); + } + } + }; + + const navItems = [ + { href: "/app/messages", label: "Messages", icon: }, + { href: "/app/treasury", label: "Treasury", icon: }, + { href: "/app/proposals", label: "Proposals", icon: }, + ]; + + const displayAddress = publicKey + ? `${publicKey.slice(0, 4)}...${publicKey.slice(-4)}` + : ""; + + return ( +
+ {/* Sidebar Layout */} + + + {/* Main Content Area */} +
+
+ {children} +
+
+
+ ); +} diff --git a/apps/web/src/app/app/messages/page.tsx b/apps/web/src/app/app/messages/page.tsx new file mode 100644 index 0000000..bee8ff3 --- /dev/null +++ b/apps/web/src/app/app/messages/page.tsx @@ -0,0 +1 @@ +export { default } from "../page"; diff --git a/apps/web/src/app/app/page.tsx b/apps/web/src/app/app/page.tsx new file mode 100644 index 0000000..0fccd04 --- /dev/null +++ b/apps/web/src/app/app/page.tsx @@ -0,0 +1,183 @@ +"use client"; + +import React, { useState } from "react"; + +interface Message { + id: string; + sender: string; + avatar: string; + text: string; + timestamp: string; + isSelf: boolean; + tokenTransfer?: { + amount: string; + token: string; + txHash: string; + }; +} + +export default function MessagesPage() { + const [messages, setMessages] = useState([ + { + id: "1", + sender: "Jed McCaleb", + avatar: "J", + text: "Hey! Did you check out the new stellar-core upgrade? The transaction speeds are looking incredibly solid.", + timestamp: "10:24 AM", + isSelf: false, + }, + { + id: "2", + sender: "You", + avatar: "Y", + text: "Yes! The ledger close times are consistently under 4 seconds now. Just sent some test transactions.", + timestamp: "10:26 AM", + isSelf: true, + }, + { + id: "3", + sender: "Jed McCaleb", + avatar: "J", + text: "Awesome. I've sent you the 50 XLM for the contract review. Let me know when you receive it.", + timestamp: "10:27 AM", + isSelf: false, + tokenTransfer: { + amount: "50 XLM", + token: "Stellar Lumens", + txHash: "0x78ab...e912", + }, + }, + ]); + + const [inputText, setInputText] = useState(""); + + const handleSendMessage = (e: React.FormEvent) => { + e.preventDefault(); + if (!inputText.trim()) return; + + const newMessage: Message = { + id: Date.now().toString(), + sender: "You", + avatar: "Y", + text: inputText, + timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + isSelf: true, + }; + + setMessages([...messages, newMessage]); + setInputText(""); + }; + + return ( +
+ {/* Top Header */} +
+
+
+
+ J +
+ +
+
+

Jed McCaleb

+

Active now • GC3K...7Z8P

+
+
+
+ {/* Quick Pay Action Button */} + +
+
+ + {/* Messages Scroll Area */} +
+ {messages.map((msg) => ( +
+ {/* Avatar */} +
+
+ {msg.avatar} +
+
+ + {/* Message Bubble Container */} +
+
+ {msg.text} + + {/* Optional token transfer attachment */} + {msg.tokenTransfer && ( +
+
+
+ + + +
+
+

Received {msg.tokenTransfer.amount}

+

{msg.tokenTransfer.token}

+
+
+ + {msg.tokenTransfer.txHash} + +
+ )} +
+ + {msg.timestamp} + +
+
+ ))} +
+ + {/* Input Form */} +
+ setInputText(e.target.value)} + placeholder="Type a secure message..." + className="flex-1 bg-[#13131f]/60 hover:bg-[#13131f]/80 focus:bg-[#13131f] border border-border focus:border-accent rounded-2xl px-5 py-3.5 text-sm focus:outline-none transition-all duration-300 placeholder:text-foreground/30" + /> + +
+
+ ); +} diff --git a/apps/web/src/app/app/profile/page.tsx b/apps/web/src/app/app/profile/page.tsx new file mode 100644 index 0000000..f6caa38 --- /dev/null +++ b/apps/web/src/app/app/profile/page.tsx @@ -0,0 +1,398 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { useAuth } from '@/components/auth/useAuth'; +import { apiFetch } from '@/lib/api'; + +type FieldErrors = { + username?: string; + avatarUrl?: string; + form?: string; +}; + +type Wallet = { + address: string; + isPrimary: boolean; +}; + +type UserProfile = { + id: string; + username: string | null; + avatarUrl: string | null; + wallets: Wallet[]; +}; + +const USERNAME_PATTERN = /^[a-zA-Z0-9_]{3,30}$/; + +function truncateAddress(address: string) { + if (address.length <= 16) return address; + return `${address.slice(0, 8)}...${address.slice(-6)}`; +} + +function getApiErrorMessage(payload: unknown) { + if ( + payload && + typeof payload === 'object' && + 'error' in payload && + typeof payload.error === 'string' + ) { + return payload.error; + } + + if ( + payload && + typeof payload === 'object' && + 'message' in payload && + typeof payload.message === 'string' + ) { + return payload.message; + } + + return 'Unable to save profile. Please try again.'; +} + +function mapApiErrors(status: number, payload: unknown): FieldErrors { + const message = getApiErrorMessage(payload); + const normalized = message.toLowerCase(); + + if (status === 409 || normalized.includes('taken') || normalized.includes('conflict')) { + return { username: 'Username is already taken.' }; + } + + if (normalized.includes('username')) { + return { username: message }; + } + + if (normalized.includes('avatar')) { + return { avatarUrl: message }; + } + + return { form: message }; +} + +export default function ProfilePage() { + const { token } = useAuth(); + const [profile, setProfile] = useState(null); + const [username, setUsername] = useState(''); + const [avatarUrl, setAvatarUrl] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [loadError, setLoadError] = useState(null); + const [fieldErrors, setFieldErrors] = useState({}); + const [showSuccessToast, setShowSuccessToast] = useState(false); + + const fetchProfile = useCallback(async () => { + if (!token) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setLoadError(null); + + try { + const response = await apiFetch('/users/me', { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error('Failed to load user profile.'); + } + + const data = (await response.json()) as UserProfile; + setProfile(data); + setUsername(data.username ?? ''); + setAvatarUrl(data.avatarUrl ?? ''); + } catch (error) { + setLoadError(error instanceof Error ? error.message : 'Failed to load user profile.'); + } finally { + setIsLoading(false); + } + }, [token]); + + useEffect(() => { + void fetchProfile(); + }, [fetchProfile]); + + useEffect(() => { + if (!showSuccessToast) return; + + const timeoutId = window.setTimeout(() => { + setShowSuccessToast(false); + }, 3000); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [showSuccessToast]); + + function validateForm() { + const nextErrors: FieldErrors = {}; + const trimmedUsername = username.trim(); + const trimmedAvatarUrl = avatarUrl.trim(); + + if (!trimmedUsername) { + nextErrors.username = 'Username is required.'; + } else if (!USERNAME_PATTERN.test(trimmedUsername)) { + nextErrors.username = 'Use 3-30 letters, numbers, or underscores.'; + } + + if (trimmedAvatarUrl) { + try { + const parsedUrl = new URL(trimmedAvatarUrl); + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + nextErrors.avatarUrl = 'Avatar URL must start with http:// or https://.'; + } + } catch { + nextErrors.avatarUrl = 'Enter a valid avatar URL.'; + } + } + + setFieldErrors(nextErrors); + return Object.keys(nextErrors).length === 0; + } + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setShowSuccessToast(false); + + if (!validateForm()) return; + + setIsSaving(true); + setFieldErrors({}); + + try { + const nextUsername = username.trim(); + const nextAvatarUrl = avatarUrl.trim(); + const response = await apiFetch('/users/me', { + method: 'PATCH', + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ + username: nextUsername, + avatarUrl: nextAvatarUrl || null, + }), + }); + const data = await response.json().catch(() => null); + + if (!response.ok) { + setFieldErrors(mapApiErrors(response.status, data)); + return; + } + + setProfile((currentProfile) => + currentProfile + ? { + ...currentProfile, + username: nextUsername, + avatarUrl: nextAvatarUrl || null, + } + : currentProfile, + ); + setShowSuccessToast(true); + window.dispatchEvent(new Event('profile-updated')); + } catch { + setFieldErrors({ form: 'Unable to save profile. Please try again.' }); + } finally { + setIsSaving(false); + } + } + + if (isLoading) { + return ( +
+
+

Loading your profile...

+
+ ); + } + + if (loadError) { + return ( +
+
+

Unable to load profile

+

{loadError}

+ +
+
+ ); + } + + return ( +
+
+

Profile saved

+

Your changes were saved successfully.

+
+ +
+

Profile Settings

+

+ Manage your username, avatar, and connected Stellar wallets. +

+
+ +
+
+

Edit Profile

+ +
+
+ {avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + Avatar preview { + event.currentTarget.src = `https://api.dicebear.com/7.x/bottts/svg?seed=${username || 'clicked'}`; + }} + className="h-16 w-16 rounded-full border-2 border-[var(--accent)] bg-[var(--border)] object-cover shadow-lg" + /> + ) : ( +
+ {username ? username.slice(0, 2).toUpperCase() : 'U'} +
+ )} +
+

Avatar

+

+ Use an image URL for your profile picture. +

+
+
+ +
+ + { + setUsername(event.target.value); + setFieldErrors((errors) => ({ ...errors, username: undefined, form: undefined })); + }} + aria-invalid={Boolean(fieldErrors.username)} + aria-describedby="username-help username-error" + placeholder="crypto_champ" + className={`w-full rounded-lg border bg-[var(--background)] px-4 py-2.5 text-sm outline-none transition-all focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] ${ + fieldErrors.username + ? 'border-red-500/60 focus:border-red-500 focus:ring-red-500/50' + : 'border-[var(--border)]' + }`} + /> + {fieldErrors.username ? ( +

+ {fieldErrors.username} +

+ ) : null} +

+ 3 to 30 characters. Letters, numbers, and underscores only. +

+
+ +
+ + { + setAvatarUrl(event.target.value); + setFieldErrors((errors) => ({ + ...errors, + avatarUrl: undefined, + form: undefined, + })); + }} + aria-invalid={Boolean(fieldErrors.avatarUrl)} + aria-describedby={fieldErrors.avatarUrl ? 'avatarUrl-error' : undefined} + placeholder="https://example.com/avatar.png" + className={`w-full rounded-lg border bg-[var(--background)] px-4 py-2.5 text-sm outline-none transition-all focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] ${ + fieldErrors.avatarUrl + ? 'border-red-500/60 focus:border-red-500 focus:ring-red-500/50' + : 'border-[var(--border)]' + }`} + /> + {fieldErrors.avatarUrl ? ( +

+ {fieldErrors.avatarUrl} +

+ ) : null} +
+ + {fieldErrors.form ? ( +

+ {fieldErrors.form} +

+ ) : null} + + +
+
+ +
+

Connected Wallets

+ +
+ {profile?.wallets.length ? ( + profile.wallets.map((wallet) => ( +
+

+ {truncateAddress(wallet.address)} +

+ {wallet.isPrimary ? ( + + Primary Wallet + + ) : null} +
+ )) + ) : ( +

+ No connected wallets found. +

+ )} +
+
+
+
+ ); +} diff --git a/apps/web/src/app/app/proposals/page.tsx b/apps/web/src/app/app/proposals/page.tsx new file mode 100644 index 0000000..ee9f2b0 --- /dev/null +++ b/apps/web/src/app/app/proposals/page.tsx @@ -0,0 +1,180 @@ +"use client"; + +import React, { useState } from "react"; + +interface Proposal { + id: string; + title: string; + creator: string; + description: string; + status: "Active" | "Succeeded" | "Defeated"; + yesVotes: number; + noVotes: number; + endsIn: string; + voted?: "yes" | "no"; +} + +export default function ProposalsPage() { + const [proposals, setProposals] = useState([ + { + id: "1", + title: "CP-024: Allocate 50,000 XLM for Stellar-Rust SDK Improvements", + creator: "0xDeon", + description: "Upgrade the Stellar Rust SDK to improve memory safety and efficiency for smart contracts, introducing robust bindings and better transaction helpers.", + status: "Active", + yesVotes: 324000, + noVotes: 42000, + endsIn: "2 days left", + }, + { + id: "2", + title: "CP-023: Deploy Multi-Sig Messaging Vault V2", + creator: "Jed McCaleb", + description: "Migrate current community multisig wallets to the audited V2 standard, adding instant chat-based transaction signing flows directly through the UI.", + status: "Succeeded", + yesVotes: 512000, + noVotes: 12000, + endsIn: "Ended 1 day ago", + }, + { + id: "3", + title: "CP-022: Increase Validator Quorum to 7 Members", + creator: "StellarDev", + description: "Proposed increase of validator consensus threshold nodes from 5 to 7 to improve fault tolerance and absolute decentralization metrics.", + status: "Defeated", + yesVotes: 110000, + noVotes: 240000, + endsIn: "Ended 5 days ago", + }, + ]); + + const handleVote = (id: string, type: "yes" | "no") => { + setProposals( + proposals.map((prop) => { + if (prop.id !== id || prop.status !== "Active" || prop.voted) return prop; + return { + ...prop, + voted: type, + yesVotes: type === "yes" ? prop.yesVotes + 10000 : prop.yesVotes, + noVotes: type === "no" ? prop.noVotes + 10000 : prop.noVotes, + }; + }) + ); + }; + + return ( +
+ {/* Page Header */} +
+
+

+ Governance Proposals +

+

Vote on community improvements and treasury resource allocations.

+
+ +
+ + {/* Proposals list */} +
+ {proposals.map((prop) => { + const totalVotes = prop.yesVotes + prop.noVotes; + const yesPercent = totalVotes > 0 ? Math.round((prop.yesVotes / totalVotes) * 100) : 0; + const noPercent = totalVotes > 0 ? 100 - yesPercent : 0; + + return ( +
+ {/* Status Badge */} +
+ + Created by {prop.creator} + + + {prop.status} + +
+ + {/* Title & Description */} +

+ {prop.title} +

+

+ {prop.description} +

+ + {/* Progress & Voting controls */} +
+ {/* Voting stats bars */} +
+
+ + + Yes ({yesPercent}%) + + + No ({noPercent}%) + + +
+
+
+
+
+
+ {prop.yesVotes.toLocaleString()} XLM + {prop.noVotes.toLocaleString()} XLM +
+
+ + {/* Vote actions */} +
+ + {prop.endsIn} + + + {prop.status === "Active" && ( + <> + {prop.voted ? ( + + Voted {prop.voted === "yes" ? "Yes" : "No"} + + ) : ( +
+ + +
+ )} + + )} +
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/app/app/treasury/page.tsx b/apps/web/src/app/app/treasury/page.tsx new file mode 100644 index 0000000..94af279 --- /dev/null +++ b/apps/web/src/app/app/treasury/page.tsx @@ -0,0 +1,111 @@ +"use client"; + +import React from "react"; + +export default function TreasuryPage() { + const assets = [ + { name: "Stellar Lumens", symbol: "XLM", balance: "420,500 XLM", value: "$42,050.00", percentage: "65%", color: "bg-accent" }, + { name: "USD Coin", symbol: "USDC", balance: "18,200 USDC", value: "$18,200.00", percentage: "28%", color: "bg-emerald-500" }, + { name: "AQUA", symbol: "AQUA", balance: "1,250,000 AQUA", value: "$4,500.00", percentage: "7%", color: "bg-cyan-500" }, + ]; + + const transactions = [ + { id: "1", type: "Disbursement", desc: "Proposal #14 Funding", amount: "-15,000 XLM", date: "June 02, 2026", status: "Completed", statusColor: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20" }, + { id: "2", type: "Deposit", desc: "DAO Staking Rewards", amount: "+4,200 XLM", date: "May 31, 2026", status: "Completed", statusColor: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20" }, + { id: "3", type: "Disbursement", desc: "Developer Grant - Phase 1", amount: "-10,000 USDC", date: "May 28, 2026", status: "Completed", statusColor: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20" }, + ]; + + return ( +
+ {/* Page Header */} +
+

+ Treasury Vault +

+

Manage and track your DAO's multi-signature assets on Stellar.

+
+ + {/* Summary Cards */} +
+
+
+

Total Vault Value

+

$64,750.00

+ + +4.2% (24h) + +
+ +
+
+

Active Multi-sig Signers

+

3 of 5

+

Threshold: 3 signatures required

+
+ +
+
+

Pending Transactions

+

0

+

All sign-offs completed

+
+
+ +
+ {/* Assets List */} +
+

Asset Allocation

+
+ {assets.map((asset) => ( +
+
+
+

{asset.name}

+

{asset.balance}

+
+
+

{asset.value}

+

{asset.percentage}

+
+
+
+
+
+
+ ))} +
+
+ + {/* Recent Transactions */} +
+

Recent Activity

+
+ {transactions.map((tx) => ( +
+
+
+ {tx.type[0]} +
+
+

{tx.desc}

+

{tx.date} • {tx.type}

+
+
+
+

+ {tx.amount} +

+ + {tx.status} + +
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/web/src/app/chat/page.tsx b/apps/web/src/app/chat/page.tsx new file mode 100644 index 0000000..a4ba6a4 --- /dev/null +++ b/apps/web/src/app/chat/page.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import type { Socket } from "socket.io-client"; +import { useAuth } from "../../lib/auth"; +import { initSocket, closeSocket } from "../../lib/socket"; +import MessageInput from "../../components/chat/MessageInput"; +import TransferCard from "../../components/chat/TransferCard"; + +type TextMsg = { id: string; type: "text"; content: string; sender: { username: string } }; +type TransferMsg = { + id: string; + type: "transfer"; + amount: number; + token?: string; + txHash: string; + sender: { username: string }; +}; +type Msg = TextMsg | TransferMsg; + +type MessageSender = { username?: string }; +type SocketMessage = { + id?: string; + content?: string; + sender?: MessageSender; + [key: string]: unknown; +}; + +function formatError(err: unknown): string { + if (err instanceof Error) { + return err.message; + } + + return typeof err === "string" ? err : "An unexpected error occurred"; +} + +export default function ChatPage() { + const { token, isLoading: authLoading } = useAuth(); + const [socket, setSocket] = useState(null); + const [conversationId] = useState("test-convo-1"); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const parseMessage = useCallback((msg: SocketMessage | null | undefined): Msg | null => { + if (!msg) return null; + + const content = typeof msg.content === "string" ? msg.content : ""; + const senderName = typeof msg.sender?.username === "string" ? msg.sender.username : "unknown"; + const sender = { username: senderName }; + + try { + const parsed = JSON.parse(content) as Partial & { + type?: string; + txHash?: string; + amount?: number | string; + token?: string; + }; + + if (parsed?.type === "transfer" && typeof parsed.txHash === "string") { + const amountValue = typeof parsed.amount === "number" ? parsed.amount : Number(parsed.amount); + + return { + id: typeof msg.id === "string" ? msg.id : `${sender.username}-${Date.now()}`, + type: "transfer", + amount: Number.isFinite(amountValue) ? amountValue : 0, + token: typeof parsed.token === "string" ? parsed.token : undefined, + txHash: parsed.txHash, + sender, + }; + } + } catch { + // Not JSON, treat as plain text + } + + return { + id: typeof msg.id === "string" ? msg.id : `${sender.username}-${Date.now()}`, + type: "text", + content, + sender, + }; + }, []); + + useEffect(() => { + if (!token || authLoading) return; + + try { + const s = initSocket(token); + const frame = window.requestAnimationFrame(() => { + setSocket(s); + }); + + s.on("new_message", (msg: SocketMessage) => { + const parsedMsg = parseMessage(msg); + if (parsedMsg) { + setMessages((prev) => [...prev, parsedMsg]); + } + }); + + s.on("room_joined", ({ conversationId: cid }: { conversationId: string }) => { + console.log("Joined room:", cid); + s.emit("message_history", { conversationId: cid }); + }); + + s.on("message_history", (data: { messages?: SocketMessage[] }) => { + const history = data.messages || []; + const parsed = history + .map((msg) => parseMessage(msg)) + .filter((m): m is Msg => m !== null); + setMessages(parsed.reverse()); + setLoading(false); + }); + + s.on("error", (err: unknown) => { + console.error("Socket error:", err); + setError(formatError(err)); + }); + + s.emit("join_room", { conversationId }); + + return () => { + window.cancelAnimationFrame(frame); + closeSocket(); + }; + } catch (err: unknown) { + const frame = window.requestAnimationFrame(() => { + setError(formatError(err)); + setLoading(false); + }); + + return () => window.cancelAnimationFrame(frame); + } + }, [token, authLoading, conversationId, parseMessage]); + + const recipient = "GDESTRECIPIENTEXAMPLEXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + + if (authLoading) { + return ( +
+
Loading...
+
+ ); + } + + if (!token) { + return ( +
+
+
No authentication token found
+

+ Please log in first, or set NEXT_PUBLIC_AUTH_TOKEN in .env.local +

+
+
+ ); + } + + if (loading) { + return ( +
+
Connecting to chat...
+
+ ); + } + + return ( +
+
+

Chat

+ + {socket?.connected ? "Connected ✓" : "Disconnected"} + +
+ + {error && ( +
{error}
+ )} + +
+ {messages.length === 0 ? ( +
No messages yet. Start a conversation!
+ ) : ( + messages.map((m) => ( +
+
{m.sender.username}
+
+ {m.type === "text" ? ( +
{m.content}
+ ) : ( + + )} +
+
+ )) + )} +
+ + +
+ ); +} diff --git a/apps/web/src/app/conversations/[id]/page.tsx b/apps/web/src/app/conversations/[id]/page.tsx new file mode 100644 index 0000000..686ac93 --- /dev/null +++ b/apps/web/src/app/conversations/[id]/page.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useEffect, useRef, useState, useCallback } from "react"; +import Image from "next/image"; +import { useParams } from "next/navigation"; +import { useSocket } from "@/hooks/useSocket"; + +interface Sender { + id: string; + username: string | null; + avatarUrl: string | null; +} + +interface Message { + id: string; + conversationId: string; + senderId: string; + content: string; + createdAt: string; + sender: Sender; +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function formatTime(iso: string) { + return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +function formatDateLabel(iso: string) { + const d = new Date(iso); + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + + if (d.toDateString() === today.toDateString()) return "Today"; + if (d.toDateString() === yesterday.toDateString()) return "Yesterday"; + return d.toLocaleDateString([], { month: "long", day: "numeric", year: "numeric" }); +} + +function dayKey(iso: string) { + return new Date(iso).toDateString(); +} + +function Avatar({ src, name }: { src: string | null; name: string }) { + if (src) { + return ( + {name} + ); + } + return ( +
+ {name.charAt(0).toUpperCase()} +
+ ); +} + +// ── Component ───────────────────────────────────────────────────────────────── + +export default function ConversationPage() { + const { id } = useParams<{ id: string }>(); + + // TODO: replace with real auth token from your auth context/store + const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; + // TODO: replace with real current user id from your auth context/store + const currentUserId = + typeof window !== "undefined" ? localStorage.getItem("userId") : null; + + const socket = useSocket(token); + const [messages, setMessages] = useState([]); + const bottomRef = useRef(null); + const containerRef = useRef(null); + + // Scroll to bottom only when user is already near the bottom + const scrollToBottom = useCallback((force = false) => { + const el = containerRef.current; + if (!el) return; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; + if (force || atBottom) { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, []); + + useEffect(() => { + if (!socket) return; + + socket.emit("join_room", { conversationId: id }); + socket.emit("message_history", { conversationId: id }); + + socket.on( + "message_history", + (data: { conversationId: string; messages: Message[] }) => { + if (data.conversationId === id) { + setMessages(data.messages); + // Force scroll on initial load + setTimeout(() => scrollToBottom(true), 50); + } + } + ); + + socket.on("new_message", (msg: Message) => { + if (msg.conversationId === id) { + setMessages((prev) => [...prev, msg]); + scrollToBottom(); + } + }); + + return () => { + socket.off("message_history"); + socket.off("new_message"); + }; + }, [socket, id, scrollToBottom]); + + // ── Group messages by day ────────────────────────────────────────────────── + const grouped: { label: string; messages: Message[] }[] = []; + for (const msg of messages) { + const key = dayKey(msg.createdAt); + const last = grouped[grouped.length - 1]; + if (last && dayKey(last.messages[0].createdAt) === key) { + last.messages.push(msg); + } else { + grouped.push({ label: formatDateLabel(msg.createdAt), messages: [msg] }); + } + } + + return ( +
+ {/* Header */} +
+

Conversation

+
+ + {/* Message thread */} +
+ {grouped.map((group) => ( +
+ {/* Date separator */} +
+
+ + {group.label} + +
+
+ +
+ {group.messages.map((msg) => { + const isSelf = msg.senderId === currentUserId; + const name = msg.sender.username ?? "Unknown"; + + return ( +
+ {!isSelf && } + +
+ {!isSelf && ( + + {name} + + )} +
+ {msg.content} +
+ + {formatTime(msg.createdAt)} + +
+ + {isSelf && } +
+ ); + })} +
+
+ ))} + +
+
+
+ ); +} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index a2dc41e..c02943f 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1,26 +1,34 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #0a0a0f; + --foreground: #f0f0f5; + --accent: #7c5cfc; + --accent-light: #a78bfa; + --muted: #3f3f50; + --card: #13131f; + --border: #1e1e2e; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); + --color-accent: var(--accent); + --color-accent-light: var(--accent-light); + --color-muted: var(--muted); + --color-card: var(--card); + --color-border: var(--border); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), Arial, Helvetica, sans-serif; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 976eb90..2bc4970 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,20 +1,25 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { ToastProvider } from "@/components/ToastProvider"; + +import { AuthProvider } from "@/components/auth/AuthProvider"; +import { WalletProvider } from "@/contexts/WalletContext"; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: 'Clicked — Web3 Social Messaging', + description: + 'Chat, send tokens, and fund ideas — all in one place. A decentralized messaging platform built on Stellar.', }; export default function RootLayout({ @@ -23,11 +28,14 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - {children} + + + + + {children} + + + ); } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 3f36f7c..6aefe85 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,65 +1,28 @@ -import Image from "next/image"; +import { Navbar } from "@/components/landing/Navbar"; +import { Hero } from "@/components/landing/Hero"; +import { Features } from "@/components/landing/Features"; +import { HowItWorks } from "@/components/landing/HowItWorks"; +import { TechStack } from "@/components/landing/TechStack"; +import { CTA } from "@/components/landing/CTA"; +import { ToastDemo } from "@/components/ToastDemo"; +import { Footer } from "@/components/landing/Footer"; +import { ProtectedRoute } from "@/components/auth/ProtectedRoute"; -export default function Home() { +export default function LandingPage() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
+ + <> + +
+ + + + + + +
+