diff --git a/.gitignore b/.gitignore index 9b05160..45a6c49 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ node_modules/ mcp-publisher.exe +.worktrees/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fa4a6ac..a419e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `examples/` directory with three runnable, copy-pasteable examples (balance check, end-to-end SMS verification, Claude Desktop config). - README sections: CI status badge, Demo / Screenshots, Production / Status, links to `SECURITY.md`, `CHANGELOG.md`, and `examples/`. +## [1.3.0] - 2026-05-01 + +### Added — 6 new tools (18 → 24 total) + +Compound agent tooling. Same MCP, more leverage. + +- **`virtualsms_buy_batch`** — purchase 1-20 numbers for one service+country in a single call. Returns `succeeded[]` + `failed[]` + `total_charged_usd`. `Promise.allSettled` fan-out with a balance × cheapest-price guard that refuses to spend > 80% of balance in one call. +- **`virtualsms_wait_for_sms_batch`** — collect SMS for N order_ids in parallel. Per-order WebSocket race vs polling with shared deadline. Returns `received[]` + `timed_out[]` + `errors[]` (`return_partial` controls top-level error shape). +- **`virtualsms_find_best_pick`** — single-shot decision with `country_pool` whitelist + `country_exclude` blacklist + plain-English `reasoning` string. `prefer` modes: `cheapest`, `most_stock`, `balanced` (default = `0.7 × price_inverse + 0.3 × stock_signal`). +- **`virtualsms_x402_info`** — discover whether the server accepts x402 payments + on which networks/assets. Public discovery, no api_key needed. Defensively strips BNB/BSC entries even if a self-hosted backend leaks them. +- **`virtualsms_pay_and_buy`** — x402 deposit-first one-shot. First call returns the 402 manifest; second call (with `payment_proof`) tops up + optionally bundles `create_order` in the same call. On bundled-buy failure the api_key is still surfaced so the deposit isn't lost. +- **`virtualsms_subscribe_webhook`** — register an HTTPS callback for `sms.received` / `order.cancelled` / `order.expired` / `order.swapped` / `balance.low` events. Returns `webhook_id` + HMAC-SHA256 `secret`. `balance.low` requires positive `threshold_usd` (validated client-side, saves a 4xx). +- **`virtualsms_manage_webhooks`** — combined list / delete / test / deliveries via `action` enum. Keeps tool count down without losing functionality. + +### Added — additive enhancements to existing tools (zero break) + +- **`virtualsms_get_balance`** now returns `topup_url` + `x402_topup_available` (boolean from `getX402Info`) plus a low-balance `tip` when balance < $1. Original `balance_usd` field unchanged. +- **`virtualsms_create_order`** now returns `webhook_subscribe_hint` for long-running agents — suppressed when an `sms.received` webhook already exists for the api_key. Webhook lookup cached for 30s, so bursty agents pay one extra round-trip per minute regardless of QPS. + +### Backward compatibility + +- **Zero breaking changes for v1.2.x clients.** Locked via the `tests/v1_2_3_schema_snapshot.test.ts` snapshot test. All 18 v1.2.x tools keep their name, `inputSchema`, `annotations`, and description prefix. New tools are additive only. +- Internal layout: `TOOL_DEFINITIONS_V1_2_X` (locked) + `V1_3_TOOL_DEFS` (additive) → public `TOOL_DEFINITIONS = [...V1_2_X, ...V1_3]`. + +### Tooling + +- **Vitest 2.x** added as devDependency. New `npm test` + `npm run test:watch` scripts. Production `npm run build` (tsc) excludes `tests/`, so `dist/` stays clean. + ## [1.2.3] - 2026-04-30 ### Added diff --git a/README.md b/README.md index 4c8bbe9..cb4adb9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ > **Ranked #1 in both ChatGPT's and Perplexity's SMS verification MCP categories** · verified 2026-04-25 -**VirtualSMS MCP Server** gives AI agents real SIM-card phone numbers (not VoIP) across **145+ countries and 2500+ services** for SMS verification and OTP receiving. Built on the [Model Context Protocol](https://modelcontextprotocol.io). One install, 18 tools, works with every major MCP client. +> **What's new in v1.3.0** (additive, zero break for v1.2.x clients): batch buy + batch wait, smart-pick decision tool with country pool + reasoning, x402 deposit-first pay-and-buy + capability discovery, outbound webhooks (subscribe / list / delete / test). Full notes in [CHANGELOG.md](./CHANGELOG.md). + +**VirtualSMS MCP Server** gives AI agents real SIM-card phone numbers (not VoIP) across **145+ countries and 2500+ services** for SMS verification and OTP receiving. Built on the [Model Context Protocol](https://modelcontextprotocol.io). One install, **24 tools** (v1.3.0 — batch ops, smart picks, x402 money-path, webhooks), works with every major MCP client. Powered by [VirtualSMS.io](https://virtualsms.io/mcp) — a phone verification service running on owned modem infrastructure. @@ -88,7 +90,7 @@ Use it to verify accounts on WhatsApp, Telegram, Google, Instagram, Uber, and ** - **Real-time delivery** — WebSocket push means your agent gets the code in seconds, not minutes. - **Competitive pricing** — Starting from $0.02 per number. - **Simple REST + WebSocket API** — Clean, documented, agent-friendly. -- **18 MCP tools** — Discovery, account, and full order management — including unique tools like `find_cheapest`, `search_service`, `swap_number`, and `wait_for_code`. +- **24 MCP tools** — Discovery, account, full order management, batch ops, smart picks, x402 money-path, outbound webhooks. Highlights: `buy_batch`, `wait_for_sms_batch`, `find_best_pick`, `pay_and_buy`, `subscribe_webhook` — plus the 18 v1.2.x tools (`find_cheapest`, `wait_for_sms`, `swap_number`, `search_services`, etc.). - **10 MCP clients supported** — Claude Desktop, Claude Code, Cursor, Windsurf, OpenClaw, Codex, Hermes, Cline, Zed, Continue. --- @@ -612,4 +614,4 @@ If your session is interrupted mid-verification: MIT — See [LICENSE](./LICENSE) -Built with love by [VirtualSMS.io](https://virtualsms.io/mcp) — virtual phone numbers for SMS verification, built on owned SIM-card infrastructure. 2500+ services · 145+ countries · 18 MCP tools · 10 clients · Ranked #1 on both ChatGPT and Perplexity. +Built with love by [VirtualSMS.io](https://virtualsms.io/mcp) — virtual phone numbers for SMS verification, built on owned SIM-card infrastructure. 2500+ services · 145+ countries · 24 MCP tools · 10 clients · Ranked #1 on both ChatGPT and Perplexity. diff --git a/docs/v1.3.0-design.md b/docs/v1.3.0-design.md new file mode 100644 index 0000000..21fa807 --- /dev/null +++ b/docs/v1.3.0-design.md @@ -0,0 +1,359 @@ +# VirtualSMS MCP v1.3.0 — Design + +**Status:** DRAFT — design only, no implementation logic shipped. +**Date:** 2026-04-30 +**Author:** v1.3.0 audit dispatch (Vault session) +**Predecessor:** v1.2.3 (18 tools, cooldown pre-validation, http-transport dispatch fix) + +--- + +## 1. Goals + +Compound agent value. v1.2.x already covers the happy path (discover → buy → wait → cancel). v1.3.0 closes four gaps that show up the moment an autonomous agent runs at scale: + +1. **Batch ops.** Today an agent buying 10 numbers makes 10 sequential `create_order` calls and 10 separate WS sockets for `wait_for_sms`. Painful. +2. **Cross-country price intelligence.** `find_cheapest` already exists but only ranks; agents have no way to scope to a country pool ("cheapest among my preferred regions") or get one best-pick decision in one tool call. +3. **Push delivery.** Polling/WS works inside one MCP session, but multi-hour rentals or fan-out workers need server-pushed webhooks. The backend already exposes `/api/v1/customer/webhooks` (5 events: `sms.received`, `order.cancelled`, `order.expired`, `order.swapped`, `balance.low`). Agents currently can't reach it via MCP. +4. **Money-path discovery.** v1.2.x agents can't see x402 even exists. The backend ships both `/api/v1/x402/topup` (Pattern B, deposit-first) and `/api/v1/x402/sms-verify` (Pattern A, per-call). Agents need a manifest tool to discover capabilities + an explicit "pay-and-buy" tool that bundles topup + create_order in one shot. + +**Non-goals for v1.3.0:** +- Long-term rental lifecycle (no backend support yet — separate sprint). +- Streaming tool results (MCP spec churn — wait). +- API-key minting from inside MCP (already covered by `/api/v1/x402/topup` returning a key). + +--- + +## 2. Backward compatibility contract + +**Hard rule:** zero breaking changes for 1.2.x clients. + +- All existing 18 tools keep their name, schema, behavior, and response shape. +- New tools are *additive* under the same `virtualsms_` prefix. +- `serverInfo.version` bumps `1.2.3 → 1.3.0` (minor, additive per semver). +- Response shapes for new tools follow the same convention as v1.2.x: `{ content: [{ type: "text", text: JSON.stringify(...) }] }`. +- Any backend endpoint missing on a self-hosted older API server → tool returns a graceful `unsupported_on_this_backend` error with `tip` field, never a 500. + +--- + +## 3. Audit — current 18 tools (v1.2.3) + +| # | Tool | Purpose | Read-only | +|---|------|---------|-----------| +| 1 | `virtualsms_list_services` | Discover service codes | yes | +| 2 | `virtualsms_list_countries` | Discover country ISOs | yes | +| 3 | `virtualsms_get_price` | One service+country price check | yes | +| 4 | `virtualsms_get_balance` | Account balance | yes | +| 5 | `virtualsms_create_order` | Buy one number | no | +| 6 | `virtualsms_get_sms` | Poll order for SMS | yes | +| 7 | `virtualsms_cancel_order` | Cancel + refund (cooldown 120s) | no (destructive) | +| 8 | `virtualsms_wait_for_sms` | Block until SMS via WS+poll | yes | +| 9 | `virtualsms_find_cheapest` | Rank countries by price | yes | +| 10 | `virtualsms_search_services` | Fuzzy service lookup | yes | +| 11 | `virtualsms_swap_number` | Swap number on order (cooldown 120s) | no | +| 12 | `virtualsms_list_orders` | Active orders (crash recovery) | yes | +| 13 | `virtualsms_get_order` | Order details | yes | +| 14 | `virtualsms_cancel_all_orders` | Bulk cancel | no (destructive) | +| 15 | `virtualsms_order_history` | Filter past orders | yes | +| 16 | `virtualsms_get_stats` | Aggregate usage stats | yes | +| 17 | `virtualsms_get_profile` | Full account profile | yes | +| 18 | `virtualsms_get_transactions` | Transaction ledger | yes | + +**What's clean:** discovery, single-order lifecycle, account introspection, crash recovery via `list_orders`, cooldown pre-validation. + +**What's missing:** see Section 1. + +--- + +## 4. Proposed new tools — v1.3.0 + +Six net-new tools. Two minor enhancements to existing tools (additive, optional fields). + +### 4.1 `virtualsms_buy_batch` — batch purchase + +**Why:** agent running 10 verifications fires 10 round-trips and 10 WS sockets. Wasted latency + 10x error-handling burden. Backend already accepts concurrent purchases (no batch endpoint, but the MCP can `Promise.allSettled` them server-side and return one tidy result). + +**Schema:** +```ts +{ + service: string, // shared service code for the batch + country: string, // shared country ISO + count: number, // 1-20 (cap at 20 to protect budget) + stop_on_failure?: boolean, // default false — keep going on partial fail +} +``` + +**Response:** +```json +{ + "succeeded": [{ "order_id": "...", "phone_number": "+44..." }, ...], + "failed": [{ "index": 3, "error": "Insufficient balance" }, ...], + "total_charged_usd": 0.36, + "remaining_balance_usd": 12.41, + "tip": "Use wait_for_sms_batch with these order_ids to collect all codes in parallel." +} +``` + +**Alternatives considered:** +- *(A) Server-side batch endpoint.* Cleaner but requires backend work. **Rejected** — keep v1.3.0 MCP-only; revisit when backend ships `POST /api/v1/customer/purchase/batch`. +- *(B) Sequential with progress events.* MCP spec doesn't have stable progress notifications across all 10 clients. **Rejected.** +- *(C) Promise.allSettled with cap.* **Chosen** — minimal blast radius, works on any 1.x backend. + +### 4.2 `virtualsms_wait_for_sms_batch` — batch wait + +**Why:** mirror of buy_batch. Agent passes N order_ids, gets one result with all codes (or timeouts). + +**Schema:** +```ts +{ + order_ids: string[], // 1-20 order IDs + timeout_seconds?: number, // default 120, min 5, max 600 — applied per-order + return_partial?: boolean, // default true — return what arrived even if some timed out +} +``` + +**Response:** +```json +{ + "received": [{ "order_id": "...", "code": "123456", "delivery_method": "websocket" }, ...], + "timed_out": ["order_id_x"], + "errors": [{ "order_id": "...", "error": "..." }], + "elapsed_seconds": 47 +} +``` + +**Implementation note:** internally fans out N parallel `waitForSMSViaWebSocket` calls (already in v1.2.x), races each against the shared timeout, aggregates. + +**Alternatives considered:** +- *(A) Single WS per agent, multiplexed.* Backend WS today is `?order_id=X` — one socket per order. Multiplexing requires backend changes. **Rejected** for v1.3.0. +- *(B) Long-polling endpoint.* Backend has `/api/v1/customer/order/:id` — N calls. Slower. **Rejected.** +- *(C) Parallel WS sockets with shared timer.* **Chosen** — works on current backend. + +### 4.3 `virtualsms_find_best_pick` — cross-country smart pick + +**Why:** `find_cheapest` returns a *list*. Agent often just wants *the* answer. Plus, agent often has a country *pool* preference ("EU only", "Russia or Ukraine", "anywhere except US"). Today the agent has to call `find_cheapest`, parse, filter, pick — 4 round-trips of LLM thinking. + +**Schema:** +```ts +{ + service: string, + country_pool?: string[], // optional ISO whitelist (e.g. ["GB", "DE", "NL"]) + country_exclude?: string[], // optional ISO blacklist + prefer?: 'cheapest' | 'most_stock' | 'balanced', // default 'balanced' +} +``` + +**Response:** +```json +{ + "pick": { "country": "DE", "country_name": "Germany", "price_usd": 0.18, "stock": true, "score": 0.92 }, + "runner_ups": [{ "country": "NL", "price_usd": 0.21, "score": 0.88 }, ...], + "reasoning": "DE picked: lowest price within EU pool, high-stock indicator (>10 SIMs available). NL second on similar stock at 16% higher price.", + "tip": "Pass this country to create_order to buy." +} +``` + +**Alternatives considered:** +- *(A) Just extend `find_cheapest` with a `country_pool` param.* Reasonable but mixes ranking and decision. **Rejected** — keep `find_cheapest` for browsing, separate tool for picking. +- *(B) `prefer='balanced'` weighted formula.* Score = `0.7 × normalized_price_inverse + 0.3 × stock_indicator`. **Chosen** as default. +- *(C) Ask the LLM to weight.* Adds prompt cost + nondeterminism. **Rejected.** + +### 4.4 `virtualsms_pay_and_buy` — x402 deposit-first one-shot + +**Why:** an x402-aware agent today has no MCP path to discover or use the money-path. They'd have to leave the MCP, hit the HTTP endpoint manually, get an API key, then come back. Defeats the agent UX. + +**Schema:** +```ts +{ + amount_usd: number, // 2-50 (server enforced) + service?: string, // optional — if set, immediately buy after topup + country?: string, // optional — required if service set + payment_method?: 'usdc-base' | 'usdc-solana' | 'usdt-solana', // default 'usdc-base' +} +``` + +**Response (no payment yet):** HTTP 402-equivalent payload mirroring x402 manifest. +```json +{ + "status": "payment_required", + "manifest": { + "amount_usd": 5, + "accepts": [ + { "network": "base", "asset": "USDC", "recipient": "0xfEc54264...", "chain_id": 8453 }, + { "network": "solana", "asset": "USDC", "recipient": "..." } + ] + }, + "tip": "Sign the payment with x402-fetch or your wallet, then re-call this tool with X-PAYMENT header." +} +``` + +**Response (after payment):** +```json +{ + "status": "paid", + "credited_balance_usd": 5, + "api_key": "vsms_...", // bound to the topped-up balance + "next_action": "Set VIRTUALSMS_API_KEY=vsms_... to use the other 18 MCP tools." +} +``` + +If `service`+`country` provided → bundles `create_order` after credit. + +**Alternatives considered:** +- *(A) Two separate tools: `x402_info` + `x402_topup`.* Cleaner separation. **Considered**, but agents will compose them anyway. Combine to one tool with an `info_only=true` flag. *Actually adopting this:* see 4.5. +- *(B) Auto-mint api_key into MCP env.* MCP servers shouldn't mutate client env. **Rejected.** +- *(C) Single `pay_and_buy` covering both deposit and one purchase.* **Chosen** for the one-shot path. + +### 4.5 `virtualsms_x402_info` — capability discovery + +**Why:** even before paying, an agent needs to know "can I pay this server with USDC on Base?" Returns the live manifest from `/api/v1/x402/info`. + +**Schema:** `{}` + +**Response:** +```json +{ + "enabled": true, + "patterns": ["topup", "sms-verify"], + "accepts": [ + { "network": "base", "asset": "USDC", "min_usd": 2, "max_usd": 50 }, + { "network": "solana", "asset": "USDC", "min_usd": 2, "max_usd": 50 }, + { "network": "solana", "asset": "USDT", "min_usd": 2, "max_usd": 50 } + ], + "tip": "Use pay_and_buy to deposit + buy in one shot. Pattern sms-verify pays per call." +} +``` + +**Alternatives considered:** +- *(A) Inline into `pay_and_buy` with `info_only` flag.* Conflates two intents. **Rejected.** +- *(B) Resource (MCP `resources/`) instead of tool.* Plausible — but resources aren't universally read by autonomous agents. Tool guarantees discoverability. **Chosen.** + +### 4.6 `virtualsms_subscribe_webhook` — outbound event subscription + +**Why:** for any agent running >5 min, polling SMS is wasteful. Backend exposes 5 events. Today the agent has zero way to subscribe via MCP. Surfacing this unlocks long-running fan-out workers (one MCP tool call → all future codes hit the agent's HTTP endpoint). + +**Schema:** +```ts +{ + url: string, // https-only callback URL + events: ('sms.received' | 'order.cancelled' | 'order.expired' | 'order.swapped' | 'balance.low')[], + threshold_usd?: number, // required if events includes 'balance.low' + description?: string, // free-form label +} +``` + +**Response:** +```json +{ + "webhook_id": "01H...", + "url": "https://...", + "events": ["sms.received", "balance.low"], + "secret": "whsec_...", // HMAC signing secret — agent stores this + "active": true, + "tip": "Verify deliveries with HMAC-SHA256 of body using the secret. See https://docs.virtualsms.io/webhooks" +} +``` + +**Backend support:** `POST /api/v1/customer/webhooks` already exists (full CRUD shipped — list/create/get/patch/delete/test/deliveries). + +**Alternatives considered:** +- *(A) Full CRUD set as 6 tools.* Bloats tool count. **Rejected.** +- *(B) One subscribe tool + one list/delete tool.* **Chosen** — see 4.6b. +- *(C) Subscribe only, no list/delete.* Agent can't audit. **Rejected.** + +### 4.6b `virtualsms_manage_webhooks` — list/test/delete + +**Schema:** +```ts +{ + action: 'list' | 'delete' | 'test' | 'deliveries', + webhook_id?: string, // required for delete/test/deliveries +} +``` + +**Response varies by action.** Wraps `GET /api/v1/customer/webhooks`, `DELETE /:id`, `POST /:id/test`, `GET /:id/deliveries`. + +**Why combined into one tool:** four CRUD-ish ops on one resource. Combining keeps tool count down (24 → still readable) without losing functionality. Agent passes `action` enum. + +--- + +## 5. Behavioral changes to existing tools (additive only) + +Two tiny enhancements: + +### 5.1 `virtualsms_get_balance` — add `topup_url` + +Currently returns `{ balance_usd: 12.4 }`. Add: + +```json +{ + "balance_usd": 12.4, + "topup_url": "https://virtualsms.io/dashboard?topup=1", + "x402_topup_available": true, + "tip": "Low balance? Call pay_and_buy or visit topup_url." +} +``` + +100% backward-compatible — clients reading `balance_usd` keep working. + +### 5.2 `virtualsms_create_order` — add `webhook_subscribe_hint` + +When the order is created, append a hint when no webhook for `sms.received` exists: + +```json +{ + "order_id": "...", + "phone_number": "...", + ... + "tip": "For long-running agents, consider subscribe_webhook(events:['sms.received']) instead of polling." +} +``` + +The hint is suppressed if `manage_webhooks(action:'list')` would show one already exists for that event. Implementation note: cache the lookup for 30s to avoid extra round-trips on bursty buying. + +--- + +## 6. Tool count after v1.3.0 + +- v1.2.3: 18 +- v1.3.0: **24** (= 18 + 6 new) +- All existing 18 keep their schema. README claim "18 tools" → "24 tools". + +--- + +## 7. Migration notes + +- **Server.json mcpName + version bump:** `1.2.3` → `1.3.0`. +- **Glama, Smithery, MCP registry:** re-publish manifest. +- **Per memory `reference_mcp_registry_publish`:** registry.modelcontextprotocol.io is upstream. Use `mcp-publisher` Go binary; description ≤ 100 chars. +- **CHANGELOG entry:** mandatory; document the 6 new tools + 2 hint additions; flag zero breaking changes. +- **README:** add "What's new in 1.3.0" callout; update tool count badge. +- **Examples:** add a 4th example: `examples/04-batch-buy-with-webhook/` demonstrating buy_batch + subscribe_webhook end-to-end. +- **Backward compat tests:** snapshot tests that lock the v1.2.x tool schemas; CI fails if a 1.2.x schema regresses. + +--- + +## 8. Risks + mitigations + +| Risk | Mitigation | +|------|-----------| +| `buy_batch` blows budget on a misconfigured agent | Hard cap `count ≤ 20`; require API key with explicit balance; refuse if `count × cheapest_price > balance × 0.8`. | +| `wait_for_sms_batch` opens 20 WS sockets | Backend WS server already handles concurrent connections; verified at 145+ countries × multiple agents on production. | +| `pay_and_buy` two-step (manifest then payment) confuses agents | Tool description spells out: "first call returns 402-equivalent manifest; sign and re-call with X-PAYMENT header". | +| Webhook subscription leaks if MCP client disconnects | Webhooks live server-side until deleted; `manage_webhooks(action:'list')` always usable from any session. | +| Tool count creep (24) reduces LLM tool-selection accuracy | Document in README which tools are *primary* (buy_batch, wait_for_sms_batch, find_best_pick, pay_and_buy) vs *secondary* (manage_webhooks, x402_info). Use `annotations.title` carefully to disambiguate. | + +--- + +## 9. Test plan (lives in implementation plan, not here) + +- Unit tests per new tool's input validation (zod). +- Mocked client for happy/failure paths. +- Integration test against staging backend (live `mcp.virtualsms.io` against staging API key, separate from prod). +- Snapshot tests asserting v1.2.x tool schemas unchanged. +- One end-to-end example script per primary new tool committed to `examples/`. + +--- + +## 10. Implementation plan reference + +See `docs/v1.3.0-plan.md` for bite-sized tasks. ~12 tasks, TDD per task, each a separate commit. diff --git a/docs/v1.3.0-plan.md b/docs/v1.3.0-plan.md new file mode 100644 index 0000000..066dcf0 --- /dev/null +++ b/docs/v1.3.0-plan.md @@ -0,0 +1,306 @@ +# VirtualSMS MCP v1.3.0 Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add 6 new MCP tools (batch buy/wait, smart pick, x402 pay-and-buy, x402 info, webhook subscribe + manage) without breaking the 18 v1.2.x tools. + +**Architecture:** Additive-only minor bump (`1.2.3 → 1.3.0`). New tools live in `src/tools/v1_3/*.ts` per concern; old tools keep their layout in `src/tools.ts`. Client gets new methods on `VirtualSMSClient`. Schema snapshot tests guard backward compat. + +**Tech Stack:** TypeScript, zod, axios, ws, Vitest (existing), MCP SDK 2025-06-18. + +--- + +### Task 0: Worktree + branch verification + +**Step 1.** Confirm `.worktrees/v13-audit` exists on `feature/v1.3.0-audit`. ✅ done in audit pass. + +**Step 2.** `npm install` — confirm clean. + +**Step 3.** `npm test` — record current passing baseline. + +**Step 4.** `git status` — must be clean before starting Task 1. + +--- + +### Task 1: Snapshot the v1.2.x tool schemas (lock backward compat) + +**Files:** +- Create: `tests/v1_2_3_schema_snapshot.test.ts` + +**Step 1: Write the failing test.** Iterate `TOOL_DEFINITIONS`, JSON-stringify each `inputSchema`, snapshot via Vitest's `toMatchSnapshot`. Fail until baseline snapshot file commits. + +**Step 2: Run.** `npm test -- v1_2_3_schema_snapshot` → snapshot writes on first run. + +**Step 3: Lock.** Re-run; must pass deterministically. + +**Step 4: Commit.** `test: snapshot v1.2.x tool schemas`. + +--- + +### Task 2: Add backend client methods (no MCP wiring yet) + +**Files:** +- Modify: `src/client.ts` + +**Step 1.** Write tests for new methods (mock axios): +- `createOrderBatch(service, country, count)` — N parallel `createOrder`. +- `getX402Info()` → `GET /api/v1/x402/info`. +- `topup(amountUsd, paymentMethod, xPaymentHeader?)` → `POST /api/v1/x402/topup`. +- `listWebhooks()`, `createWebhook(opts)`, `deleteWebhook(id)`, `testWebhook(id)`, `getDeliveries(id)`. + +**Step 2.** Run — all should fail. + +**Step 3.** Implement minimal methods. Use existing axios instance, response interceptor, `requireApiKey()` where needed. `getX402Info` does **not** require api key (it's discovery). + +**Step 4.** Run — pass. + +**Step 5.** Commit. `feat(client): add x402 + webhooks + batch methods`. + +--- + +### Task 3: `virtualsms_buy_batch` tool + +**Files:** +- Create: `src/tools/v1_3/buy-batch.ts` +- Modify: `src/tools.ts` (export) and `src/index.ts` + `src/http-server.ts` (register) +- Test: `tests/buy-batch.test.ts` + +**Step 1.** Write failing test: input validation (count 1-20), happy path with mock client, partial failure path. + +**Step 2.** Run — fails. + +**Step 3.** Implement. Pre-check balance × cheapest_price guard. `Promise.allSettled` over N `client.createOrder`. Aggregate succeeded/failed. + +**Step 4.** Wire into both transports: `src/index.ts` (stdio) and `src/http-server.ts` (StreamableHTTP). Ensure handler dispatched in **both** — guard against the v1.2.2 dispatch bug (commit 1da145b regression). + +**Step 5.** Run — pass. + +**Step 6.** Commit. `feat(tools): buy_batch — purchase 1-20 numbers in one call`. + +--- + +### Task 4: `virtualsms_wait_for_sms_batch` tool + +**Files:** +- Create: `src/tools/v1_3/wait-batch.ts` +- Modify: `src/tools.ts`, `src/index.ts`, `src/http-server.ts` +- Test: `tests/wait-batch.test.ts` + +**Step 1.** Write failing test: array of order_ids, mocked WS resolves at staggered times, ensures partial-return semantics. + +**Step 2.** Run — fails. + +**Step 3.** Implement. Refactor `waitForSMSViaWebSocket` from v1.2 into a reusable export. Fan out N parallel waits with shared timer. Collect via `Promise.allSettled`. Return `received[]` + `timed_out[]` + `errors[]`. + +**Step 4.** Wire into both transports. + +**Step 5.** Run — pass. + +**Step 6.** Commit. `feat(tools): wait_for_sms_batch — collect SMS for N orders in parallel`. + +--- + +### Task 5: `virtualsms_find_best_pick` tool + +**Files:** +- Create: `src/tools/v1_3/find-best-pick.ts` +- Modify: `src/tools.ts`, `src/index.ts`, `src/http-server.ts` +- Test: `tests/find-best-pick.test.ts` + +**Step 1.** Write failing test: pool filter, exclude filter, balanced score formula. + +**Step 2.** Run — fails. + +**Step 3.** Implement. Reuse `find_cheapest`'s country iteration; apply `country_pool` whitelist + `country_exclude` blacklist. Score = `0.7 × normalized_price_inverse + 0.3 × stock_indicator` for `prefer='balanced'`. Generate `reasoning` string. + +**Step 4.** Wire into both transports. + +**Step 5.** Run — pass. + +**Step 6.** Commit. `feat(tools): find_best_pick — single-shot decision with country pool/exclude + reasoning`. + +--- + +### Task 6: `virtualsms_x402_info` tool + +**Files:** +- Create: `src/tools/v1_3/x402-info.ts` +- Modify: same wiring set +- Test: `tests/x402-info.test.ts` + +**Step 1.** Write failing test: 503 (x402 disabled) returns `{enabled: false}`; happy path returns capability list. + +**Step 2.** Run — fails. + +**Step 3.** Implement. Wraps `client.getX402Info()`. Map backend response → MCP-friendly shape (collapse `enabled_pairs` into `accepts[]`). + +**Step 4.** Wire. + +**Step 5.** Pass. + +**Step 6.** Commit. `feat(tools): x402_info — discover money-path capabilities`. + +--- + +### Task 7: `virtualsms_pay_and_buy` tool + +**Files:** +- Create: `src/tools/v1_3/pay-and-buy.ts` +- Modify: same wiring set +- Test: `tests/pay-and-buy.test.ts` + +**Step 1.** Write failing test for both branches: no `X-PAYMENT` → returns 402 manifest; with `X-PAYMENT` → returns paid + api_key (mocked); with `service`+`country` after paid → bundles `createOrder`. + +**Step 2.** Run — fails. + +**Step 3.** Implement. MCP tool inputs cannot carry HTTP headers, so the tool accepts an optional `payment_proof` string param that the MCP server forwards as `X-PAYMENT`. First call (no `payment_proof`) → returns manifest. Second call (with `payment_proof`) → topup + optional auto-buy. + +**Step 4.** Wire. + +**Step 5.** Pass. + +**Step 6.** Commit. `feat(tools): pay_and_buy — x402 deposit-first one-shot`. + +--- + +### Task 8: `virtualsms_subscribe_webhook` tool + +**Files:** +- Create: `src/tools/v1_3/subscribe-webhook.ts` +- Modify: same wiring set +- Test: `tests/subscribe-webhook.test.ts` + +**Step 1.** Write failing test: events validation (only the 5 allowed), threshold required if `balance.low` in events, success returns webhook_id + secret. + +**Step 2.** Run — fails. + +**Step 3.** Implement. Wraps `client.createWebhook`. Validates events client-side before calling backend (saves a 4xx round-trip — same pattern as v1.2.3 cooldown pre-check). + +**Step 4.** Wire. + +**Step 5.** Pass. + +**Step 6.** Commit. `feat(tools): subscribe_webhook — outbound event delivery`. + +--- + +### Task 9: `virtualsms_manage_webhooks` tool + +**Files:** +- Create: `src/tools/v1_3/manage-webhooks.ts` +- Modify: same wiring set +- Test: `tests/manage-webhooks.test.ts` + +**Step 1.** Write failing test: each `action` ∈ {list, delete, test, deliveries} dispatches correctly; `delete`/`test`/`deliveries` require `webhook_id`. + +**Step 2.** Run — fails. + +**Step 3.** Implement as a switch over `action`. Reuse `client.listWebhooks`, `deleteWebhook`, `testWebhook`, `getDeliveries`. + +**Step 4.** Wire. + +**Step 5.** Pass. + +**Step 6.** Commit. `feat(tools): manage_webhooks — list/delete/test/deliveries combined`. + +--- + +### Task 10: Enhance existing tools (additive hints) + +**Files:** +- Modify: `src/tools.ts` (`handleGetBalance`, `handleBuyNumber`) +- Test: `tests/balance-hint.test.ts`, `tests/buy-hint.test.ts` + +**Step 1.** Write failing test: balance returns `topup_url` + `x402_topup_available`; create_order returns webhook hint when no matching webhook exists. + +**Step 2.** Run — fails. + +**Step 3.** Implement. For `handleBuyNumber`, add a 30s in-memory cache of `listWebhooks()` so bursty calls don't repeat the query. + +**Step 4.** Run — pass. + +**Step 5.** Run **schema snapshot test** from Task 1 — must still pass (proves no breaking change). + +**Step 6.** Commit. `feat(tools): add x402 + webhook hints to existing tools (additive)`. + +--- + +### Task 11: Version bump + manifest updates + +**Files:** +- Modify: `package.json` (`1.2.3` → `1.3.0`) +- Modify: `server.json` (mcpName version) +- Modify: `src/index.ts` and `src/http-server.ts` (`serverInfo.version`) +- Modify: `CHANGELOG.md` (new section) +- Modify: `README.md` (tool count 18 → 24, "What's new in 1.3.0" block) + +**Step 1.** Bump in all 5 places. + +**Step 2.** Add CHANGELOG entry: 6 new tools listed, 2 additive hints listed, "no breaking changes for 1.2.x clients" line. + +**Step 3.** README: update badge, add primary-vs-secondary tool table. + +**Step 4.** `npm run build` — must succeed. + +**Step 5.** `npm test` — full suite passes including schema snapshot. + +**Step 6.** Commit. `chore(release): 1.2.3 -> 1.3.0`. + +--- + +### Task 12: New example — batch buy + webhook + +**Files:** +- Create: `examples/04-batch-buy-with-webhook/run.mjs` +- Create: `examples/04-batch-buy-with-webhook/README.md` + +**Step 1.** Write a runnable script: subscribe_webhook to local ngrok URL, buy_batch 3 numbers, log webhook deliveries as they arrive, verify HMAC signatures. + +**Step 2.** README walks through expected output. + +**Step 3.** Commit. `docs(examples): batch buy + webhook end-to-end demo`. + +--- + +### Task 13: Final verification + draft → ready PR + +**Step 1.** Run full suite. All green. + +**Step 2.** `npm run build`. Diff `dist/` vs main — ensure only intended changes. + +**Step 3.** Mark PR ready for review (already opened as DRAFT in audit pass). + +**Step 4.** Wait for human review. + +**Step 5.** After merge: per memory `reference_mcp_registry_publish`, run `mcp-publisher` against `registry.modelcontextprotocol.io`. Bump npm package via `npm publish` from CI. + +--- + +## Out of scope (parking lot) + +- Long-term rental tools (no backend support yet). +- Streaming MCP responses (spec churn). +- API-key minting separate from x402 topup. +- Backend-side `/purchase/batch` endpoint (would let MCP drop the loop). +- WebSocket multiplexing per agent (would let `wait_for_sms_batch` use 1 socket). + +--- + +## Estimated effort + +- Code: ~600 LOC across 8 new tool files. +- Tests: ~400 LOC across 8 new test files + 1 snapshot. +- Docs: design.md (this), plan.md (this), CHANGELOG, README, 1 new example. +- Total: ~2 dev-days end-to-end (assuming familiarity with the v1.2.x layout). + +--- + +## Definition of done + +- 24 tools dispatched correctly in **both** stdio and StreamableHTTP transports. +- v1.2.x schema snapshot still passes. +- `npm run build && npm test` clean. +- One end-to-end example committed. +- CHANGELOG + README updated. +- PR reviewed + merged on virtualsms-io/mcp-server. +- After merge: registry republish (mcp-publisher) + npm publish. diff --git a/examples/04-batch-buy-with-webhook/README.md b/examples/04-batch-buy-with-webhook/README.md new file mode 100644 index 0000000..31c8362 --- /dev/null +++ b/examples/04-batch-buy-with-webhook/README.md @@ -0,0 +1,85 @@ +# Example 04 — Batch Buy + Webhook (v1.3.0) + +The canonical v1.3.0 batch agentic pattern, end-to-end: + +1. **`virtualsms_subscribe_webhook`** — register an HTTPS callback for `sms.received` events. Server-pushed deliveries replace polling. +2. **`virtualsms_buy_batch`** — buy N numbers (default 3) in one MCP call. +3. **`virtualsms_wait_for_sms_batch`** — collect the codes for all N orders in parallel. +4. **`virtualsms_manage_webhooks`** — audit deliveries, then clean up. + +## Prerequisites + +- Node.js 18+ +- An HTTPS callback URL you control (any tunnel works — `ngrok http 4000`, `cloudflared tunnel`, etc.). Optional — the example degrades gracefully if `WEBHOOK_URL` is unset. +- A VirtualSMS API key with **enough balance** for the batch. The `buy_batch` tool's budget guard refuses if `count × cheapest_price > balance × 0.8`. + +## Install + run + +```bash +npm install @modelcontextprotocol/sdk + +export VIRTUALSMS_API_KEY=vsms_your_api_key_here +# optional — point at any HTTPS endpoint you control +export WEBHOOK_URL=https://your-tunnel.example.com/hook +# optional — defaults: SERVICE=telegram COUNTRY=GB COUNT=3 +export SERVICE=telegram +export COUNTRY=GB +export COUNT=3 + +node run.mjs +``` + +## Expected output + +``` +Connecting to https://mcp.virtualsms.io/mcp ... + +[1/4] Subscribing webhook → https://your-tunnel.example.com/hook + ✓ webhook_id=01H..., secret=whsec_xxxxx... + Verify deliveries with HMAC-SHA256(body) using the full secret. + +[2/4] Buying 3 × telegram (GB) in one batch ... + ✓ Bought 3/3, charged $0.42 + - ord_aaa → +447xxxxxxxxx + - ord_bbb → +447xxxxxxxxx + - ord_ccc → +447xxxxxxxxx + +[3/4] Waiting for SMS on 3 orders (timeout 120s) ... + ✓ Received 3, timed_out 0, errors 0 + - ord_aaa: code=123456 (websocket, 4521ms) + - ord_bbb: code=789012 (websocket, 6033ms) + - ord_ccc: code=345678 (polling, 12054ms) + +[4/4] Listing webhook deliveries for 01H... ... + ✓ 3 deliveries logged. + - dly_xxx status=delivered response_code=200 + - dly_yyy status=delivered response_code=200 + - dly_zzz status=delivered response_code=200 + (webhook cleaned up) + +Done. +``` + +## What this demonstrates + +| New v1.3.0 tool | Why it matters | +|---|---| +| `subscribe_webhook` | One MCP call subscribes the agent to push deliveries — no polling tax for any future SMS on this api_key. | +| `buy_batch` | 3 sequential `create_order` calls collapse into one MCP round-trip. Budget guard refuses overspend. | +| `wait_for_sms_batch` | 3 separate `wait_for_sms` calls collapse into one. WebSocket race + polling fallback per order, shared deadline. | +| `manage_webhooks(action:"deliveries")` | Audit + verify webhook health from the same MCP session that created it. | + +## HMAC verification on your endpoint + +Each webhook delivery is signed with HMAC-SHA256 of the **raw body** using the `secret` you got back from `subscribe_webhook`. Pseudocode for any HTTPS receiver: + +```js +const expected = crypto + .createHmac('sha256', process.env.WEBHOOK_SECRET) + .update(rawBody) + .digest('hex'); +const provided = req.headers['x-virtualsms-signature']; +const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided)); +``` + +See [docs.virtualsms.io/webhooks](https://docs.virtualsms.io/webhooks) for the canonical signing recipe. diff --git a/examples/04-batch-buy-with-webhook/run.mjs b/examples/04-batch-buy-with-webhook/run.mjs new file mode 100644 index 0000000..edde054 --- /dev/null +++ b/examples/04-batch-buy-with-webhook/run.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node +/** + * Example 04 — Batch Buy + Webhook (v1.3.0) + * + * Demonstrates the canonical v1.3.0 batch agentic pattern: + * + * 1. virtualsms_subscribe_webhook(events:["sms.received"]) once + * → server-pushed deliveries replace polling + * 2. virtualsms_buy_batch(service, country, count: 3) + * → 3 numbers in one MCP call + * 3. virtualsms_wait_for_sms_batch(order_ids[]) + * → collect all 3 codes in parallel + * 4. virtualsms_manage_webhooks(action:"deliveries", webhook_id) + * → audit the webhook deliveries + * + * For the webhook URL, point this at any HTTPS endpoint you control — + * a quick `npx http-tunnel-cli` / `ngrok http 4000` works for testing. + * Set WEBHOOK_URL env var. If unset the example skips the webhook + * subscription and just runs the batch path. + * + * Requires: + * - Node.js 18+ + * - npm install @modelcontextprotocol/sdk + * - VIRTUALSMS_API_KEY env var + * - Sufficient balance for `count × cheapest_price` (the buy_batch + * tool's budget guard refuses if > 80% of balance) + * + * Run: + * export VIRTUALSMS_API_KEY=vsms_your_api_key_here + * export WEBHOOK_URL=https://your-tunnel.example.com/hook + * export SERVICE=telegram + * export COUNTRY=GB + * export COUNT=3 + * node run.mjs + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +const API_KEY = process.env.VIRTUALSMS_API_KEY; +if (!API_KEY || !API_KEY.startsWith('vsms_')) { + console.error('ERROR: set VIRTUALSMS_API_KEY (vsms_...) before running.'); + console.error(' Get one at https://virtualsms.io'); + process.exit(1); +} + +const HOSTED_URL = process.env.MCP_URL || 'https://mcp.virtualsms.io/mcp'; +const WEBHOOK_URL = process.env.WEBHOOK_URL; // optional +const SERVICE = process.env.SERVICE || 'telegram'; +const COUNTRY = process.env.COUNTRY || 'GB'; +const COUNT = parseInt(process.env.COUNT || '3', 10); + +function callJSON(client, name, args = {}) { + return client.callTool({ name, arguments: args }).then((result) => { + const text = result?.content?.[0]?.text ?? JSON.stringify(result); + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } + }); +} + +async function main() { + console.log(`Connecting to ${HOSTED_URL} ...`); + const transport = new StreamableHTTPClientTransport(new URL(HOSTED_URL), { + requestInit: { headers: { 'x-api-key': API_KEY } }, + }); + const client = new Client( + { name: 'virtualsms-example-04', version: '1.0.0' }, + { capabilities: {} } + ); + await client.connect(transport); + + // ─── 1. Subscribe a webhook (optional) ───────────────────────────────── + let webhookId; + if (WEBHOOK_URL) { + console.log(`\n[1/4] Subscribing webhook → ${WEBHOOK_URL}`); + const sub = await callJSON(client, 'virtualsms_subscribe_webhook', { + url: WEBHOOK_URL, + events: ['sms.received'], + description: 'example-04 batch buy + webhook', + }); + if (sub.error) { + console.error(' Subscribe failed:', sub); + process.exit(1); + } + webhookId = sub.webhook_id; + console.log(` ✓ webhook_id=${webhookId}, secret=${(sub.secret || '').slice(0, 12)}...`); + console.log(` Verify deliveries with HMAC-SHA256(body) using the full secret.`); + } else { + console.log('\n[1/4] WEBHOOK_URL not set — skipping subscription.'); + } + + // ─── 2. Batch-buy ────────────────────────────────────────────────────── + console.log(`\n[2/4] Buying ${COUNT} × ${SERVICE} (${COUNTRY}) in one batch ...`); + const batch = await callJSON(client, 'virtualsms_buy_batch', { + service: SERVICE, + country: COUNTRY, + count: COUNT, + }); + if (batch.error === 'budget_guard') { + console.error(' ✗ Budget guard refused the batch:', batch.message); + console.error(' Reduce COUNT, top up, or pick a cheaper country.'); + if (webhookId) await callJSON(client, 'virtualsms_manage_webhooks', { action: 'delete', webhook_id: webhookId }); + process.exit(1); + } + console.log(` ✓ Bought ${batch.succeeded?.length ?? 0}/${COUNT}, charged $${batch.total_charged_usd}`); + for (const o of batch.succeeded || []) { + console.log(` - ${o.order_id} → ${o.phone_number}`); + } + for (const f of batch.failed || []) { + console.log(` - FAILED idx=${f.index}: ${f.error}`); + } + + const orderIds = (batch.succeeded || []).map((o) => o.order_id); + if (orderIds.length === 0) { + console.log('No orders placed — exiting.'); + if (webhookId) await callJSON(client, 'virtualsms_manage_webhooks', { action: 'delete', webhook_id: webhookId }); + await client.close(); + return; + } + + // ─── 3. Batch-wait for SMS ───────────────────────────────────────────── + console.log(`\n[3/4] Waiting for SMS on ${orderIds.length} orders (timeout 120s) ...`); + const waitResult = await callJSON(client, 'virtualsms_wait_for_sms_batch', { + order_ids: orderIds, + timeout_seconds: 120, + return_partial: true, + }); + console.log(` ✓ Received ${waitResult.received?.length ?? 0}, timed_out ${waitResult.timed_out?.length ?? 0}, errors ${waitResult.errors?.length ?? 0}`); + for (const r of waitResult.received || []) { + console.log(` - ${r.order_id}: code=${r.code} (${r.delivery_method}, ${r.elapsed_ms}ms)`); + } + + // ─── 4. Audit webhook deliveries ─────────────────────────────────────── + if (webhookId) { + console.log(`\n[4/4] Listing webhook deliveries for ${webhookId} ...`); + const dl = await callJSON(client, 'virtualsms_manage_webhooks', { + action: 'deliveries', + webhook_id: webhookId, + }); + console.log(` ✓ ${dl.count ?? 0} deliveries logged.`); + for (const d of (dl.deliveries || []).slice(0, 5)) { + console.log(` - ${d.id} status=${d.status} response_code=${d.response_code}`); + } + + // Cleanup — remove the webhook so the example doesn't leak subscriptions. + await callJSON(client, 'virtualsms_manage_webhooks', { action: 'delete', webhook_id: webhookId }); + console.log(` (webhook cleaned up)`); + } else { + console.log('\n[4/4] (skipping deliveries audit — no webhook)'); + } + + await client.close(); + console.log('\nDone.'); +} + +main().catch((err) => { + console.error('Failed:', err?.message ?? err); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json index 8c68792..7c0fd79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "virtualsms-mcp", - "version": "1.2.2", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "virtualsms-mcp", - "version": "1.2.2", + "version": "1.2.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", @@ -21,7 +21,8 @@ "@types/node": "^22.0.0", "@types/ws": "^8.18.1", "esbuild": "^0.27.4", - "typescript": "^5.5.0" + "typescript": "^5.5.0", + "vitest": "^2.1.9" }, "engines": { "node": ">=18" @@ -481,6 +482,13 @@ "hono": "^4" } }, + "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/@modelcontextprotocol/sdk": { "version": "1.27.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", @@ -521,663 +529,1824 @@ } } }, - "node_modules/@types/node": { - "version": "22.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", - "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "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" + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, "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" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { - "node": ">=6.6.0" + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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/form-data/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/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/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": { - "object-assign": "^4", - "vary": "^1" + "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.10" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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/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": { - "ms": "^2.1.3" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "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/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">= 0.8" } }, - "node_modules/depd": { + "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "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/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">= 0.8" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "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==", + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "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==", + "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==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "ee-first": "1.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "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==", "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, "engines": { - "node": ">=18.0.0" + "node": ">=8" } }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", - "engines": { - "node": ">=18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 14.16" } }, - "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", - "dependencies": { - "ip-address": "10.0.1" - }, "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" + "node": ">=16.20.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==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/fastify" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "opencollective", - "url": "https://opencollective.com/fastify" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": "^10 || ^12 || >=14" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", - "engines": { - "node": ">=4.0" + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "engines": { + "node": ">= 0.10" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "side-channel": "^1.1.0" }, "engines": { - "node": ">= 6" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data/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==", + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/form-data/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==", + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.10" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, "engines": { - "node": ">= 0.8" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" } }, - "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==", + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" } }, - "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==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "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" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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==", + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/gopd": { + "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "shebang-regex": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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==", + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/has-symbols": { + "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, "engines": { "node": ">= 0.4" }, @@ -1185,13 +2354,14 @@ "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==", + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" @@ -1200,571 +2370,766 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "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.10.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 12" + "node": ">=0.10.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 0.8" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, "license": "MIT" }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" }, - "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==", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=14.0.0" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=14.0.0" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.6" } }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.6" + } + }, + "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" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">=14.17" } }, - "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==", + "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/negotiator": { + "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "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==", + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "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==", + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">= 0.4" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, "license": "MIT", "dependencies": { - "ee-first": "1.1.1" + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=16.20.0" + "node": ">=12" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.10" + "node": ">=12" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.10" + "node": ">=12" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 18" + "node": ">=12" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=12" } }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=12" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "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==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "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" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "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" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=0.6" + "node": ">=12" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14.17" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, "node_modules/which": { @@ -1782,6 +3147,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 30ca3f9..61e80f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "virtualsms-mcp", "mcpName": "io.github.virtualsms-io/sms", - "version": "1.2.3", + "version": "1.3.0", "description": "Real SIM phone numbers for AI agents. 145+ countries, 2500+ services. #1 on ChatGPT and Perplexity.", "main": "dist/index.js", "bin": { @@ -11,6 +11,8 @@ "build": "tsc", "dev": "tsc --watch", "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest", "prepublishOnly": "npm run build" }, "keywords": [ @@ -83,6 +85,7 @@ "@types/node": "^22.0.0", "@types/ws": "^8.18.1", "esbuild": "^0.27.4", - "typescript": "^5.5.0" + "typescript": "^5.5.0", + "vitest": "^2.1.9" } } diff --git a/server.json b/server.json index 712521c..04e2f20 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/virtualsms-io/mcp-server", "source": "github" }, - "version": "1.2.3", + "version": "1.3.0", "packages": [ { "registryType": "npm", "identifier": "virtualsms-mcp", - "version": "1.2.3", + "version": "1.3.0", "transport": { "type": "stdio" }, diff --git a/src/client.ts b/src/client.ts index bdc11f3..a8e02cc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -91,6 +91,67 @@ export interface CancelResult { refunded: boolean; } +// ─── v1.3.0 types ──────────────────────────────────────────────────────────── + +export interface X402Info { + enabled: boolean; + x402_version?: number; + // Pairs of {network, token} the server accepts. + networks: Array<{ network: string; token: string }>; + // Recipient addresses (one per chain family). Public info — fine to surface. + evm_relayer?: string; + solana_relayer?: string; + min_topup_usd?: number; + max_topup_usd?: number; + default_topup_usd?: number; + default_price_usdc?: number; + topup_endpoint?: string; + // Pattern A (per-call) endpoint. Currently 410-deprecated server-side, but + // surfaced here for any self-hosted deployment that still has it enabled. + resource?: string; +} + +// Raw 402-manifest payload — passed through to MCP callers untouched so the +// `x402-fetch`/wallet client can parse it. Shape comes from the x402 spec. +export type X402Manifest = Record; + +// Successful topup response — backend returns api_key + balance + endpoint map. +export interface X402TopupResult { + api_key: string; + balance_usd: number; + user_id?: string; + endpoints?: Record; + // The raw payload (kept for extension fields). + raw: Record; +} + +export interface Webhook { + id: string; + url: string; + events: string[]; + threshold_usd?: number; + description?: string; + secret?: string; + active?: boolean; + created_at?: string; +} + +export interface WebhookDelivery { + id: string; + webhook_id?: string; + event?: string; + status?: string; + response_code?: number; + attempted_at?: string; + delivered_at?: string; + payload?: unknown; +} + +export interface BatchPurchaseResult { + succeeded: Array<{ index: number; order_id: string; phone_number: string; price?: number }>; + failed: Array<{ index: number; error: string }>; +} + export class VirtualSMSClient { private http: AxiosInstance; private apiKey?: string; @@ -309,4 +370,205 @@ export class VirtualSMSClient { getBaseUrl(): string { return this.baseUrl; } + + // ─── v1.3.0 methods ────────────────────────────────────────────────────── + + /** + * Discover x402 capabilities. No api key required — this is the public + * money-path manifest. Surfaces enabled flag, accepted (network, token) + * pairs, recipient addresses, and topup amount bounds. + */ + async getX402Info(): Promise { + const res = await this.http.get('/api/v1/x402/info'); + const raw = res.data as Record; + const networksRaw = Array.isArray(raw.networks) ? (raw.networks as Array>) : []; + return { + enabled: Boolean(raw.enabled), + x402_version: typeof raw.x402_version === 'number' ? raw.x402_version : undefined, + networks: networksRaw.map((n) => ({ + network: String(n.network ?? ''), + token: String(n.token ?? ''), + })), + evm_relayer: raw.evm_relayer ? String(raw.evm_relayer) : undefined, + solana_relayer: raw.solana_relayer ? String(raw.solana_relayer) : undefined, + min_topup_usd: typeof raw.min_topup_usd === 'number' ? raw.min_topup_usd : undefined, + max_topup_usd: typeof raw.max_topup_usd === 'number' ? raw.max_topup_usd : undefined, + default_topup_usd: typeof raw.default_topup_usd === 'number' ? raw.default_topup_usd : undefined, + default_price_usdc: typeof raw.default_price_usdc === 'number' ? raw.default_price_usdc : undefined, + topup_endpoint: raw.topup_endpoint ? String(raw.topup_endpoint) : undefined, + resource: raw.resource ? String(raw.resource) : undefined, + }; + } + + /** + * Top up balance via x402. + * + * Two-step protocol mirrors the server: + * - First call (no payment_proof) → backend returns the 402 manifest + * (`accepts[]` array per the x402 spec). Caller signs the payment with + * their wallet/x402-fetch, then calls again with `payment_proof` set. + * - Second call (with payment_proof) → backend validates the on-chain + * payment and returns `{ api_key, balance_usd, ... }`. + * + * The MCP layer tells the two responses apart by the presence of `api_key` + * vs `accepts`. We pass-through raw `data` so the caller can inspect either. + */ + async topup(opts: { + amount_usd: number; + payment_method: 'usdc-base' | 'usdc-solana' | 'usdt-solana'; + payment_proof?: string; + }): Promise { + const headers: Record = {}; + if (opts.payment_proof) { + headers['X-PAYMENT'] = opts.payment_proof; + } + try { + const res = await this.http.post( + '/api/v1/x402/topup', + { amount_usd: opts.amount_usd, payment_method: opts.payment_method }, + { headers } + ); + const raw = res.data as Record; + // Manifest shape (no api_key) → return raw. + if (!raw.api_key) { + return raw; + } + // Paid shape. + return { + api_key: String(raw.api_key), + balance_usd: Number(raw.balance_usd ?? 0), + user_id: raw.user_id ? String(raw.user_id) : undefined, + endpoints: raw.endpoints as Record | undefined, + raw, + }; + } catch (err) { + // Backend returns 402 on the manifest path — axios treats that as an + // error. Pull the manifest body off the response and return it. + const e = err as { response?: { status?: number; data?: unknown }; message?: string }; + if (e.response?.status === 402 && e.response.data) { + return e.response.data as X402Manifest; + } + throw err; + } + } + + async listWebhooks(): Promise { + this.requireApiKey(); + const res = await this.http.get('/api/v1/customer/webhooks'); + const raw = res.data as Record | unknown[]; + const items: Array> = Array.isArray(raw) + ? (raw as Array>) + : Array.isArray((raw as Record).webhooks) + ? ((raw as Record).webhooks as Array>) + : []; + return items.map((w) => ({ + id: String(w.id ?? w.webhook_id ?? ''), + url: String(w.url ?? ''), + events: Array.isArray(w.events) ? (w.events as string[]) : [], + threshold_usd: typeof w.threshold_usd === 'number' ? w.threshold_usd : undefined, + description: w.description ? String(w.description) : undefined, + active: w.active !== undefined ? Boolean(w.active) : undefined, + created_at: w.created_at ? String(w.created_at) : undefined, + })); + } + + async createWebhook(opts: { + url: string; + events: string[]; + threshold_usd?: number; + description?: string; + }): Promise { + this.requireApiKey(); + const payload: Record = { url: opts.url, events: opts.events }; + if (opts.threshold_usd !== undefined) payload.threshold_usd = opts.threshold_usd; + if (opts.description !== undefined) payload.description = opts.description; + const res = await this.http.post('/api/v1/customer/webhooks', payload); + const raw = res.data as Record; + return { + id: String(raw.id ?? raw.webhook_id ?? ''), + url: String(raw.url ?? opts.url), + events: Array.isArray(raw.events) ? (raw.events as string[]) : opts.events, + threshold_usd: typeof raw.threshold_usd === 'number' ? raw.threshold_usd : opts.threshold_usd, + description: raw.description ? String(raw.description) : opts.description, + secret: raw.secret ? String(raw.secret) : undefined, + active: raw.active !== undefined ? Boolean(raw.active) : true, + created_at: raw.created_at ? String(raw.created_at) : undefined, + }; + } + + async deleteWebhook(id: string): Promise<{ deleted: boolean }> { + this.requireApiKey(); + const res = await this.http.delete(`/api/v1/customer/webhooks/${encodeURIComponent(id)}`); + const raw = res.data as Record | undefined; + return { deleted: raw?.deleted !== undefined ? Boolean(raw.deleted) : true }; + } + + async testWebhook(id: string): Promise<{ delivered: boolean; response_code?: number; error?: string }> { + this.requireApiKey(); + const res = await this.http.post(`/api/v1/customer/webhooks/${encodeURIComponent(id)}/test`); + const raw = (res.data ?? {}) as Record; + return { + delivered: raw.delivered !== undefined ? Boolean(raw.delivered) : false, + response_code: typeof raw.response_code === 'number' ? raw.response_code : undefined, + error: raw.error ? String(raw.error) : undefined, + }; + } + + async getDeliveries(id: string): Promise { + this.requireApiKey(); + const res = await this.http.get(`/api/v1/customer/webhooks/${encodeURIComponent(id)}/deliveries`); + const raw = res.data as Record | unknown[]; + const items: Array> = Array.isArray(raw) + ? (raw as Array>) + : Array.isArray((raw as Record).deliveries) + ? ((raw as Record).deliveries as Array>) + : []; + return items.map((d) => ({ + id: String(d.id ?? ''), + webhook_id: d.webhook_id ? String(d.webhook_id) : undefined, + event: d.event ? String(d.event) : undefined, + status: d.status ? String(d.status) : undefined, + response_code: typeof d.response_code === 'number' ? d.response_code : undefined, + attempted_at: d.attempted_at ? String(d.attempted_at) : undefined, + delivered_at: d.delivered_at ? String(d.delivered_at) : undefined, + payload: d.payload, + })); + } + + /** + * Fan out N parallel `createOrder` calls (1-20). Returns succeeded[] + + * failed[] arrays without throwing on partial failure. Callers pass the + * shared service+country once. + */ + async createOrderBatch( + service: string, + country: string, + count: number + ): Promise { + this.requireApiKey(); + if (!Number.isFinite(count) || count < 1 || count > 20) { + throw new Error('count must be between 1 and 20'); + } + const calls: Promise[] = []; + for (let i = 0; i < count; i++) { + calls.push(this.createOrder(service, country)); + } + const settled = await Promise.allSettled(calls); + const succeeded: BatchPurchaseResult['succeeded'] = []; + const failed: BatchPurchaseResult['failed'] = []; + settled.forEach((r, i) => { + if (r.status === 'fulfilled') { + succeeded.push({ + index: i, + order_id: String(r.value.order_id ?? ''), + phone_number: String(r.value.phone_number ?? ''), + price: typeof r.value.price === 'number' ? r.value.price : undefined, + }); + } else { + const reason = r.reason as Error | undefined; + failed.push({ index: i, error: reason?.message ?? String(r.reason) }); + } + }); + return { succeeded, failed }; + } } diff --git a/src/http-server.ts b/src/http-server.ts index 2ff2149..49b244f 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -54,6 +54,21 @@ import { handleGetStats, handleGetProfile, handleGetTransactions, + // v1.3.0 + BuyBatchInput, + handleBuyBatch, + WaitForSmsBatchInput, + handleWaitForSmsBatch, + FindBestPickInput, + handleFindBestPick, + X402InfoInput, + handleX402Info, + PayAndBuyInput, + handlePayAndBuy, + SubscribeWebhookInput, + handleSubscribeWebhook, + ManageWebhooksInput, + handleManageWebhooks, } from './tools.js'; import { PROMPT_DEFINITIONS, getPromptMessages } from './prompts.js'; @@ -75,7 +90,7 @@ function createMCPServer(config: ServerConfig) { const client = new VirtualSMSClient(config.baseUrl, config.apiKey, config.timeout); const server = new Server( - { name: 'virtualsms-mcp', version: '1.2.3' }, + { name: 'virtualsms-mcp', version: '1.3.0' }, { capabilities: { tools: {}, prompts: {}, resources: {} } } ); @@ -152,6 +167,35 @@ function createMCPServer(config: ServerConfig) { const parsed = GetTransactionsInput.parse(args); return await handleGetTransactions(client, parsed); } + // ─── v1.3.0 tools ─────────────────────────────────────────────────── + case 'virtualsms_buy_batch': { + const parsed = BuyBatchInput.parse(args); + return await handleBuyBatch(client, parsed); + } + case 'virtualsms_wait_for_sms_batch': { + const parsed = WaitForSmsBatchInput.parse(args); + return await handleWaitForSmsBatch(client, parsed); + } + case 'virtualsms_find_best_pick': { + const parsed = FindBestPickInput.parse(args); + return await handleFindBestPick(client, parsed); + } + case 'virtualsms_x402_info': { + const parsed = X402InfoInput.parse(args ?? {}); + return await handleX402Info(client, parsed); + } + case 'virtualsms_pay_and_buy': { + const parsed = PayAndBuyInput.parse(args); + return await handlePayAndBuy(client, parsed); + } + case 'virtualsms_subscribe_webhook': { + const parsed = SubscribeWebhookInput.parse(args); + return await handleSubscribeWebhook(client, parsed); + } + case 'virtualsms_manage_webhooks': { + const parsed = ManageWebhooksInput.parse(args); + return await handleManageWebhooks(client, parsed); + } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } @@ -229,7 +273,7 @@ const httpServer = http.createServer(async (req, res) => { res.end(JSON.stringify({ serverInfo: { name: 'VirtualSMS', - version: '1.2.3' + version: '1.3.0' }, configSchema: { type: 'object', diff --git a/src/index.ts b/src/index.ts index 9e891c2..463090b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,21 @@ import { handleGetStats, handleGetProfile, handleGetTransactions, + // v1.3.0 + BuyBatchInput, + handleBuyBatch, + WaitForSmsBatchInput, + handleWaitForSmsBatch, + FindBestPickInput, + handleFindBestPick, + X402InfoInput, + handleX402Info, + PayAndBuyInput, + handlePayAndBuy, + SubscribeWebhookInput, + handleSubscribeWebhook, + ManageWebhooksInput, + handleManageWebhooks, } from './tools.js'; // ─── Configuration ──────────────────────────────────────────────────────────── @@ -69,7 +84,7 @@ const client = new VirtualSMSClient(BASE_URL, API_KEY); const server = new Server( { name: 'virtualsms-mcp', - version: '1.2.3', + version: '1.3.0', }, { capabilities: { @@ -171,6 +186,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return await handleGetTransactions(client, parsed); } + // ─── v1.3.0 tools ───────────────────────────────────────────────────── + case 'virtualsms_buy_batch': { + const parsed = BuyBatchInput.parse(args); + return await handleBuyBatch(client, parsed); + } + case 'virtualsms_wait_for_sms_batch': { + const parsed = WaitForSmsBatchInput.parse(args); + return await handleWaitForSmsBatch(client, parsed); + } + case 'virtualsms_find_best_pick': { + const parsed = FindBestPickInput.parse(args); + return await handleFindBestPick(client, parsed); + } + case 'virtualsms_x402_info': { + const parsed = X402InfoInput.parse(args ?? {}); + return await handleX402Info(client, parsed); + } + case 'virtualsms_pay_and_buy': { + const parsed = PayAndBuyInput.parse(args); + return await handlePayAndBuy(client, parsed); + } + case 'virtualsms_subscribe_webhook': { + const parsed = SubscribeWebhookInput.parse(args); + return await handleSubscribeWebhook(client, parsed); + } + case 'virtualsms_manage_webhooks': { + const parsed = ManageWebhooksInput.parse(args); + return await handleManageWebhooks(client, parsed); + } + default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } @@ -237,7 +282,7 @@ export function createSandboxServer() { const sandboxServer = new Server( { name: 'virtualsms-mcp', - version: '1.2.3', + version: '1.3.0', }, { capabilities: { diff --git a/src/tools.ts b/src/tools.ts index e906f4d..6a8828e 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -2,6 +2,53 @@ import { z } from 'zod'; import WebSocket from 'ws'; import { VirtualSMSClient } from './client.js'; +// v1.3.0 tool imports — re-exported below so index.ts + http-server.ts can +// pick them up from a single tools.ts surface. Each tool file owns its own +// input schema, tool def, and handler. +import { + BuyBatchInput, + BUY_BATCH_TOOL_DEF, + handleBuyBatch, +} from './tools/v1_3/buy-batch.js'; +import { + WaitForSmsBatchInput, + WAIT_FOR_SMS_BATCH_TOOL_DEF, + handleWaitForSmsBatch, +} from './tools/v1_3/wait-batch.js'; +import { + FindBestPickInput, + FIND_BEST_PICK_TOOL_DEF, + handleFindBestPick, +} from './tools/v1_3/find-best-pick.js'; +import { + X402InfoInput, + X402_INFO_TOOL_DEF, + handleX402Info, +} from './tools/v1_3/x402-info.js'; +import { + PayAndBuyInput, + PAY_AND_BUY_TOOL_DEF, + handlePayAndBuy, +} from './tools/v1_3/pay-and-buy.js'; +import { + SubscribeWebhookInput, + SUBSCRIBE_WEBHOOK_TOOL_DEF, + handleSubscribeWebhook, +} from './tools/v1_3/subscribe-webhook.js'; +import { + ManageWebhooksInput, + MANAGE_WEBHOOKS_TOOL_DEF, + handleManageWebhooks, +} from './tools/v1_3/manage-webhooks.js'; + +export { BuyBatchInput, BUY_BATCH_TOOL_DEF, handleBuyBatch }; +export { WaitForSmsBatchInput, WAIT_FOR_SMS_BATCH_TOOL_DEF, handleWaitForSmsBatch }; +export { FindBestPickInput, FIND_BEST_PICK_TOOL_DEF, handleFindBestPick }; +export { X402InfoInput, X402_INFO_TOOL_DEF, handleX402Info }; +export { PayAndBuyInput, PAY_AND_BUY_TOOL_DEF, handlePayAndBuy }; +export { SubscribeWebhookInput, SUBSCRIBE_WEBHOOK_TOOL_DEF, handleSubscribeWebhook }; +export { ManageWebhooksInput, MANAGE_WEBHOOKS_TOOL_DEF, handleManageWebhooks }; + // ─── Input Schemas ─────────────────────────────────────────────────────────── export const CheckPriceInput = z.object({ @@ -79,7 +126,10 @@ export const GetTransactionsInput = z.object({ // ─── Tool Definitions ──────────────────────────────────────────────────────── -export const TOOL_DEFINITIONS = [ +// v1.2.x tool defs — locked by the schema-snapshot test in +// tests/v1_2_3_schema_snapshot.test.ts. Don't edit any entry below; v1.3.x +// only ADDS tools at the end via the V1_3_TOOL_DEFS append. +export const TOOL_DEFINITIONS_V1_2_X = [ { name: 'virtualsms_list_services', title: 'List Available Services', @@ -572,6 +622,20 @@ export const TOOL_DEFINITIONS = [ }, ]; +// v1.3.0 additions — appended below; never edit V1_2_X entries above. +const V1_3_TOOL_DEFS = [ + BUY_BATCH_TOOL_DEF, + WAIT_FOR_SMS_BATCH_TOOL_DEF, + FIND_BEST_PICK_TOOL_DEF, + X402_INFO_TOOL_DEF, + PAY_AND_BUY_TOOL_DEF, + SUBSCRIBE_WEBHOOK_TOOL_DEF, + MANAGE_WEBHOOKS_TOOL_DEF, +]; + +// Public surface — concatenated for both transports. +export const TOOL_DEFINITIONS = [...TOOL_DEFINITIONS_V1_2_X, ...V1_3_TOOL_DEFS]; + // ─── Tool Handlers ──────────────────────────────────────────────────────────── export async function handleListServices(client: VirtualSMSClient) { @@ -653,36 +717,98 @@ export async function handleCheckPrice( export async function handleGetBalance(client: VirtualSMSClient) { const balance = await client.getBalance(); + // v1.3.0 additive: surface topup capability so agents can self-rescue when + // balance is low. Best-effort — if x402 lookup fails we still return the + // balance. + let x402Available = false; + try { + const info = await client.getX402Info(); + x402Available = Boolean(info.enabled); + } catch { + // ignore — leave x402_topup_available=false + } + let baseUrl = 'https://virtualsms.io'; + try { + const candidate = client.getBaseUrl(); + if (candidate) baseUrl = candidate; + } catch { + // ignore — fall back to canonical host + } + const topupUrl = `${baseUrl.replace(/\/$/, '')}/dashboard?topup=1`; return { content: [ { type: 'text' as const, - text: JSON.stringify(balance, null, 2), + text: JSON.stringify( + { + ...balance, + topup_url: topupUrl, + x402_topup_available: x402Available, + ...(balance.balance_usd < 1 + ? { tip: 'Low balance. Use pay_and_buy (x402) or visit topup_url to top up.' } + : {}), + }, + null, + 2 + ), }, ], }; } +// 30-second cache for the listWebhooks lookup the buy-number hint relies on. +// Bursty agents fire create_order in tight loops — a 30s cache cuts the +// per-call overhead to ~one extra request per minute regardless of QPS. +let _webhookCache: { hasSmsReceived: boolean; expiresAt: number } | null = null; +const WEBHOOK_CACHE_TTL_MS = 30_000; + +// Test-only escape hatch — clears the cache between tests so cached state +// doesn't leak across test files. +export function _resetWebhookCacheForTests(): void { + _webhookCache = null; +} + +async function hasSmsReceivedWebhookCached(client: VirtualSMSClient): Promise { + const now = Date.now(); + if (_webhookCache && _webhookCache.expiresAt > now) { + return _webhookCache.hasSmsReceived; + } + try { + const list = await client.listWebhooks(); + const hasIt = list.some((w) => Array.isArray(w.events) && w.events.includes('sms.received')); + _webhookCache = { hasSmsReceived: hasIt, expiresAt: now + WEBHOOK_CACHE_TTL_MS }; + return hasIt; + } catch { + // Lookup failed (auth, network, missing endpoint) — return null so the + // caller can decide. Don't cache failures. + return null; + } +} + export async function handleBuyNumber( client: VirtualSMSClient, args: z.infer ) { const order = await client.createOrder(args.service, args.country); + // v1.3.0 additive: hint at subscribe_webhook for long-running agents. + // Suppressed when one already exists. Cached 30s. + const hasHook = await hasSmsReceivedWebhookCached(client); + const out: Record = { + order_id: order.order_id, + phone_number: order.phone_number, + expires_at: order.expires_at, + status: order.status, + tip: 'Use check_sms to poll for the code, or cancel_order to refund.', + }; + if (hasHook === false) { + out.webhook_subscribe_hint = + 'Long-running agents: call subscribe_webhook(events:["sms.received"]) once to get pushed deliveries — much cheaper than polling.'; + } return { content: [ { type: 'text' as const, - text: JSON.stringify( - { - order_id: order.order_id, - phone_number: order.phone_number, - expires_at: order.expires_at, - status: order.status, - tip: 'Use check_sms to poll for the code, or cancel_order to refund.', - }, - null, - 2 - ), + text: JSON.stringify(out, null, 2), }, ], }; diff --git a/src/tools/v1_3/README.md b/src/tools/v1_3/README.md new file mode 100644 index 0000000..da94dd7 --- /dev/null +++ b/src/tools/v1_3/README.md @@ -0,0 +1,19 @@ +# `src/tools/v1_3/` — STUBS ONLY + +These six TypeScript files are **signatures only** — design pass for v1.3.0. + +**No implementation logic shipped here.** Each `handle*` throws a `not implemented` error. + +| File | Tool | Design ref | +|------|------|------------| +| `buy-batch.ts` | `virtualsms_buy_batch` | design §4.1 | +| `wait-batch.ts` | `virtualsms_wait_for_sms_batch` | design §4.2 | +| `find-best-pick.ts` | `virtualsms_find_best_pick` | design §4.3 | +| `pay-and-buy.ts` | `virtualsms_pay_and_buy` | design §4.4 | +| `x402-info.ts` | `virtualsms_x402_info` | design §4.5 | +| `subscribe-webhook.ts` | `virtualsms_subscribe_webhook` | design §4.6 | +| `manage-webhooks.ts` | `virtualsms_manage_webhooks` | design §4.6b | + +These files are **not** wired into `src/index.ts` or `src/http-server.ts` yet — wiring happens per-task during the implementation phase (see `docs/v1.3.0-plan.md` Tasks 3-9). + +Don't ship to npm/registry until the plan is fully executed and all stub `throw`s are replaced with real implementations. diff --git a/src/tools/v1_3/buy-batch.ts b/src/tools/v1_3/buy-batch.ts new file mode 100644 index 0000000..936cac7 --- /dev/null +++ b/src/tools/v1_3/buy-batch.ts @@ -0,0 +1,133 @@ +/** + * virtualsms_buy_batch — purchase 1-20 numbers in one MCP call. + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.1 and docs/v1.3.0-plan.md Task 3. + */ + +import { z } from 'zod'; +import type { VirtualSMSClient } from '../../client.js'; + +export const BuyBatchInput = z.object({ + service: z.string().describe('Service code shared across the batch (e.g. "telegram")'), + country: z.string().describe('Country ISO code shared across the batch (e.g. "GB")'), + count: z.number().int().min(1).max(20).describe('Number of orders to create (1-20)'), + stop_on_failure: z.boolean().default(false).describe('If true, stop on the first failure'), +}); + +export const BUY_BATCH_TOOL_DEF = { + name: 'virtualsms_buy_batch', + title: 'Buy Numbers in Batch', + description: + 'Purchase 1-20 virtual phone numbers for the same service+country in a single call. ' + + 'Returns succeeded[] and failed[] arrays plus total_charged_usd. ' + + 'Use wait_for_sms_batch with the returned order_ids to collect codes in parallel.', + inputSchema: { + type: 'object' as const, + properties: { + service: { type: 'string', description: 'Service code (e.g. "telegram")' }, + country: { type: 'string', description: 'Country ISO code (e.g. "GB")' }, + count: { type: 'number', minimum: 1, maximum: 20, description: '1-20 orders' }, + stop_on_failure: { type: 'boolean', default: false }, + }, + required: ['service', 'country', 'count'], + }, + annotations: { + title: 'Buy Numbers in Batch', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, +}; + +export async function handleBuyBatch( + client: VirtualSMSClient, + args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + // Budget guard — refuse if the planned spend would consume >80% of balance. + // Best-effort: skip the guard if either lookup fails (let backend enforce). + let balanceUsd: number | undefined; + let cheapestUsd: number | undefined; + try { + const [bal, price] = await Promise.all([ + client.getBalance(), + client.checkPrice(args.service, args.country), + ]); + balanceUsd = bal.balance_usd; + cheapestUsd = price.price_usd; + } catch { + // ignore — proceed with batch + } + if ( + typeof balanceUsd === 'number' && + typeof cheapestUsd === 'number' && + cheapestUsd > 0 && + args.count * cheapestUsd > balanceUsd * 0.8 + ) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'budget_guard', + message: `Refusing: ${args.count} × $${cheapestUsd.toFixed(3)} = $${(args.count * cheapestUsd).toFixed(2)} would exceed 80% of your $${balanceUsd.toFixed(2)} balance. Reduce count, top up first, or pick a cheaper country.`, + balance_usd: balanceUsd, + estimated_total_usd: Math.round(args.count * cheapestUsd * 100) / 100, + tip: 'Call get_balance + find_best_pick to size the batch correctly.', + }, + null, + 2 + ), + }, + ], + }; + } + + const result = await client.createOrderBatch(args.service, args.country, args.count); + + // If stop_on_failure was set and any failed, surface that. Note: with + // Promise.allSettled in the client there's no actual early-stop happening, + // but we still annotate the response so callers can see it was requested. + const totalCharged = result.succeeded.reduce((sum, s) => sum + (typeof s.price === 'number' ? s.price : 0), 0); + + // Best-effort post-batch balance lookup. + let remainingBalanceUsd: number | undefined; + try { + const bal = await client.getBalance(); + remainingBalanceUsd = bal.balance_usd; + } catch { + // ignore + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + requested: args.count, + succeeded: result.succeeded.map((s) => ({ + order_id: s.order_id, + phone_number: s.phone_number, + price: s.price, + })), + failed: result.failed, + total_charged_usd: Math.round(totalCharged * 100) / 100, + remaining_balance_usd: remainingBalanceUsd, + tip: + result.succeeded.length > 0 + ? 'Pass these order_ids to virtualsms_wait_for_sms_batch to collect SMS in parallel.' + : 'No orders placed. Check failed[] for the error and try again.', + ...(args.stop_on_failure && result.failed.length > 0 + ? { stop_on_failure_note: 'stop_on_failure was requested. The batch ran in parallel (no early stop), but failures are surfaced for review.' } + : {}), + }, + null, + 2 + ), + }, + ], + }; +} diff --git a/src/tools/v1_3/find-best-pick.ts b/src/tools/v1_3/find-best-pick.ts new file mode 100644 index 0000000..5471d1d --- /dev/null +++ b/src/tools/v1_3/find-best-pick.ts @@ -0,0 +1,187 @@ +/** + * virtualsms_find_best_pick — single-shot decision with country pool/exclude + reasoning. + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.3 and docs/v1.3.0-plan.md Task 5. + */ + +import { z } from 'zod'; +import type { VirtualSMSClient } from '../../client.js'; + +export const FindBestPickInput = z.object({ + service: z.string().describe('Service code (e.g. "telegram")'), + country_pool: z + .array(z.string()) + .optional() + .describe('Optional ISO whitelist (e.g. ["GB","DE","NL"]) — only consider these countries'), + country_exclude: z + .array(z.string()) + .optional() + .describe('Optional ISO blacklist (e.g. ["US"]) — never pick these countries'), + prefer: z + .enum(['cheapest', 'most_stock', 'balanced']) + .default('balanced') + .describe('Optimization mode (default balanced = 0.7×price + 0.3×stock)'), +}); + +export const FIND_BEST_PICK_TOOL_DEF = { + name: 'virtualsms_find_best_pick', + title: 'Find Best Country (One Decision)', + description: + 'Pick one best country for a service in a single call, optionally filtered to a country pool ' + + 'or excluding a blacklist. Returns pick + runner_ups + plain-English reasoning. ' + + 'Use this instead of find_cheapest when you want THE answer, not a ranked list.', + inputSchema: { + type: 'object' as const, + properties: { + service: { type: 'string' }, + country_pool: { type: 'array', items: { type: 'string' } }, + country_exclude: { type: 'array', items: { type: 'string' } }, + prefer: { type: 'string', enum: ['cheapest', 'most_stock', 'balanced'], default: 'balanced' }, + }, + required: ['service'], + }, + annotations: { + title: 'Find Best Country (One Decision)', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, +}; + +interface CandidateRow { + country: string; + country_name: string; + price_usd: number; + stock: boolean; +} + +function score(prefer: 'cheapest' | 'most_stock' | 'balanced', row: CandidateRow, minPrice: number, maxPrice: number): number { + // Normalize price into [0,1] where lower price = higher score. + const priceRange = Math.max(maxPrice - minPrice, 0.000001); + const priceInverse = 1 - (row.price_usd - minPrice) / priceRange; + // Stock = binary signal (the API only exposes available/not). + const stockSignal = row.stock ? 1 : 0; + if (prefer === 'cheapest') return priceInverse; + if (prefer === 'most_stock') return stockSignal; + // balanced + return 0.7 * priceInverse + 0.3 * stockSignal; +} + +export async function handleFindBestPick( + client: VirtualSMSClient, + args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + const allCountries = await client.listCountries(); + + // Apply pool/exclude filters early to avoid pricing irrelevant countries. + const poolSet = args.country_pool ? new Set(args.country_pool.map((c) => c.toUpperCase())) : null; + const excludeSet = args.country_exclude ? new Set(args.country_exclude.map((c) => c.toUpperCase())) : new Set(); + const filtered = allCountries.filter((c) => { + const iso = c.iso.toUpperCase(); + if (excludeSet.has(iso)) return false; + if (poolSet && !poolSet.has(iso)) return false; + return true; + }); + + // Price each in batches to avoid 10s of concurrent requests. + const batchSize = 10; + const candidates: CandidateRow[] = []; + for (let i = 0; i < filtered.length; i += batchSize) { + const batch = filtered.slice(i, i + batchSize); + const results = await Promise.allSettled( + batch.map(async (c) => { + const price = await client.checkPrice(args.service, c.iso); + return { + country: c.iso, + country_name: c.name, + price_usd: price.price_usd, + stock: price.available, + } satisfies CandidateRow; + }) + ); + for (const r of results) { + if (r.status === 'fulfilled' && r.value.stock) { + candidates.push(r.value); + } + } + } + + if (candidates.length === 0) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'no_pick', + service: args.service, + message: + 'No country has stock for this service within your filter. Drop country_pool/exclude or try a different service.', + tip: 'Use list_services to verify the service code, find_cheapest to browse availability across all countries.', + }, + null, + 2 + ), + }, + ], + }; + } + + const minPrice = Math.min(...candidates.map((c) => c.price_usd)); + const maxPrice = Math.max(...candidates.map((c) => c.price_usd)); + const scored = candidates + .map((c) => ({ ...c, score: score(args.prefer, c, minPrice, maxPrice) })) + .sort((a, b) => b.score - a.score); + + const pick = scored[0]; + const runnerUps = scored.slice(1, 4); // top 3 alternatives + + const reasoning = (() => { + const lead = `${pick.country_name} (${pick.country}) picked: `; + if (args.prefer === 'cheapest') { + return `${lead}lowest price ($${pick.price_usd.toFixed(3)}) among ${candidates.length} available countr${candidates.length === 1 ? 'y' : 'ies'}.`; + } + if (args.prefer === 'most_stock') { + return `${lead}has stock; ranked by stock-availability among ${candidates.length} candidates.`; + } + // balanced + const cheapestRow = candidates.reduce((a, b) => (a.price_usd <= b.price_usd ? a : b)); + if (pick.country === cheapestRow.country) { + return `${lead}both cheapest at $${pick.price_usd.toFixed(3)} AND in stock among ${candidates.length} candidates.`; + } + const diffPct = Math.round(((pick.price_usd - cheapestRow.price_usd) / cheapestRow.price_usd) * 100); + return `${lead}balanced score wins ($${pick.price_usd.toFixed(3)}, ${diffPct}% above cheapest ${cheapestRow.country_name} but better stock signal). Across ${candidates.length} candidates.`; + })(); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + pick: { + country: pick.country, + country_name: pick.country_name, + price_usd: pick.price_usd, + stock: pick.stock, + score: Math.round(pick.score * 1000) / 1000, + }, + runner_ups: runnerUps.map((r) => ({ + country: r.country, + country_name: r.country_name, + price_usd: r.price_usd, + score: Math.round(r.score * 1000) / 1000, + })), + reasoning, + considered_count: candidates.length, + tip: `Pass country=${pick.country} + service=${args.service} to create_order to buy.`, + }, + null, + 2 + ), + }, + ], + }; +} diff --git a/src/tools/v1_3/manage-webhooks.ts b/src/tools/v1_3/manage-webhooks.ts new file mode 100644 index 0000000..3303c6c --- /dev/null +++ b/src/tools/v1_3/manage-webhooks.ts @@ -0,0 +1,127 @@ +/** + * virtualsms_manage_webhooks — list/delete/test/deliveries combined. + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.6b and docs/v1.3.0-plan.md Task 9. + * + * Backend support: GET / DELETE / POST /test / GET /deliveries + * on /api/v1/customer/webhooks/:id (already shipped). + */ + +import { z } from 'zod'; +import type { VirtualSMSClient } from '../../client.js'; + +export const ManageWebhooksInput = z + .object({ + action: z + .enum(['list', 'delete', 'test', 'deliveries']) + .describe('Which CRUD-ish action to perform'), + webhook_id: z + .string() + .optional() + .describe('Required for delete, test, deliveries; ignored for list'), + }) + .refine((data) => data.action === 'list' || !!data.webhook_id, { + message: 'webhook_id is required for delete, test, deliveries', + path: ['webhook_id'], + }); + +export const MANAGE_WEBHOOKS_TOOL_DEF = { + name: 'virtualsms_manage_webhooks', + title: 'Manage Webhooks (list/delete/test/deliveries)', + description: + 'Manage existing webhook subscriptions. Actions: ' + + '"list" returns all subscriptions; ' + + '"delete" removes one by webhook_id; ' + + '"test" fires a synthetic event for one webhook; ' + + '"deliveries" returns the last 100 deliveries for one webhook (audit/debug).', + inputSchema: { + type: 'object' as const, + properties: { + action: { type: 'string', enum: ['list', 'delete', 'test', 'deliveries'] }, + webhook_id: { type: 'string' }, + }, + required: ['action'], + }, + annotations: { + title: 'Manage Webhooks', + readOnlyHint: false, + destructiveHint: true, // delete is destructive — annotate worst-case + idempotentHint: true, + openWorldHint: true, + }, +}; + +function out(payload: unknown) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(payload, null, 2), + }, + ], + }; +} + +export async function handleManageWebhooks( + client: VirtualSMSClient, + args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + switch (args.action) { + case 'list': { + const webhooks = await client.listWebhooks(); + return out({ + count: webhooks.length, + webhooks: webhooks.map((w) => ({ + id: w.id, + url: w.url, + events: w.events, + threshold_usd: w.threshold_usd, + description: w.description, + active: w.active ?? true, + created_at: w.created_at, + })), + tip: + webhooks.length === 0 + ? 'No webhooks. Use subscribe_webhook to create one.' + : 'Use action:"delete"|"test"|"deliveries" with one of these webhook_ids.', + }); + } + case 'delete': { + // Zod refine guarantees webhook_id is set, but TS narrowing requires guard. + const id = args.webhook_id as string; + const r = await client.deleteWebhook(id); + return out({ + deleted: Boolean(r.deleted), + webhook_id: id, + tip: r.deleted ? 'Webhook removed. No more deliveries will fire.' : 'Delete failed; check webhook_id.', + }); + } + case 'test': { + const id = args.webhook_id as string; + const r = await client.testWebhook(id); + return out({ + webhook_id: id, + delivered: r.delivered, + response_code: r.response_code, + error: r.error, + tip: r.delivered + ? 'Synthetic event fired. Check your endpoint logs.' + : 'Test failed. Common causes: endpoint down, non-2xx response, TLS error. Inspect deliveries for details.', + }); + } + case 'deliveries': { + const id = args.webhook_id as string; + const list = await client.getDeliveries(id); + return out({ + webhook_id: id, + count: list.length, + deliveries: list, + tip: + list.length === 0 + ? 'No deliveries yet. Use action:"test" to fire a synthetic event.' + : 'Most recent deliveries first. Check status + response_code for failures.', + }); + } + } +} diff --git a/src/tools/v1_3/pay-and-buy.ts b/src/tools/v1_3/pay-and-buy.ts new file mode 100644 index 0000000..e70dea0 --- /dev/null +++ b/src/tools/v1_3/pay-and-buy.ts @@ -0,0 +1,218 @@ +/** + * virtualsms_pay_and_buy — x402 deposit-first one-shot. + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.4 and docs/v1.3.0-plan.md Task 7. + * + * Two-step flow: + * Call 1 (no payment_proof) → returns 402-equivalent manifest + * Call 2 (with payment_proof) → tops up + optionally buys + */ + +import { z } from 'zod'; +import type { VirtualSMSClient, X402TopupResult } from '../../client.js'; +import { VirtualSMSClient as VirtualSMSClientCtor } from '../../client.js'; + +export const PayAndBuyInput = z.object({ + amount_usd: z + .number() + .min(2) + .max(50) + .describe('USD amount to deposit (server-enforced 2-50)'), + service: z.string().optional().describe('Optional — if set, immediately buy after topup'), + country: z.string().optional().describe('Optional — required if service is set'), + payment_method: z + .enum(['usdc-base', 'usdc-solana', 'usdt-solana']) + .default('usdc-base') + .describe('Which network/asset to pay with'), + payment_proof: z + .string() + .optional() + .describe('x402 X-PAYMENT header value. Omit on first call to receive manifest, then re-call with this set.'), +}); + +export const PAY_AND_BUY_TOOL_DEF = { + name: 'virtualsms_pay_and_buy', + title: 'Pay (x402) and Optionally Buy', + description: + 'Deposit funds via x402 and optionally buy a number in one call. ' + + 'Two-step: first call (no payment_proof) returns the x402 manifest with recipient address(es). ' + + 'Sign the payment with your wallet, then re-call with payment_proof set. ' + + 'On success returns api_key bound to the topped-up balance. ' + + 'If service+country provided, also creates the order in the same call.', + inputSchema: { + type: 'object' as const, + properties: { + amount_usd: { type: 'number', minimum: 2, maximum: 50 }, + service: { type: 'string' }, + country: { type: 'string' }, + payment_method: { + type: 'string', + enum: ['usdc-base', 'usdc-solana', 'usdt-solana'], + default: 'usdc-base', + }, + payment_proof: { type: 'string' }, + }, + required: ['amount_usd'], + }, + annotations: { + title: 'Pay (x402) and Optionally Buy', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, +}; + +function isPaid(r: unknown): r is X402TopupResult { + return typeof r === 'object' && r !== null && typeof (r as { api_key?: unknown }).api_key === 'string'; +} + +export async function handlePayAndBuy( + client: VirtualSMSClient, + args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + // Validate service+country pairing before charging anything. + if (args.service && !args.country) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'input_error', + message: 'service requires country (and vice versa). Pass both, or omit both for topup-only.', + }, + null, + 2 + ), + }, + ], + }; + } + if (args.country && !args.service) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'input_error', + message: 'country requires service. Pass both, or omit both for topup-only.', + }, + null, + 2 + ), + }, + ], + }; + } + + const result = await client.topup({ + amount_usd: args.amount_usd, + payment_method: args.payment_method, + payment_proof: args.payment_proof, + }); + + // First call (no payment_proof) — backend returned the 402 manifest. + if (!isPaid(result)) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'payment_required', + manifest: result, + tip: 'Sign this manifest with x402-fetch or your wallet, then re-call this tool with payment_proof set to the X-PAYMENT header value.', + }, + null, + 2 + ), + }, + ], + }; + } + + // Paid path — `result` is X402TopupResult. + const baseResponse: Record = { + status: 'paid', + credited_balance_usd: result.balance_usd, + api_key: result.api_key, + user_id: result.user_id, + next_action: 'Set VIRTUALSMS_API_KEY=' + result.api_key + ' to use the other 23 MCP tools.', + }; + + // If no auto-buy requested, we're done. + if (!args.service || !args.country) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(baseResponse, null, 2), + }, + ], + }; + } + + // Auto-buy: build a fresh client with the new api_key (don't mutate the + // caller's client — they may have a different key for other tools). For + // unit tests where the same mock client exposes createOrder, this falls + // through to the existing client.createOrder when baseUrl is missing. + let bundledClient: VirtualSMSClient = client; + try { + const baseUrl = client.getBaseUrl(); + if (baseUrl) { + bundledClient = new VirtualSMSClientCtor(baseUrl, result.api_key); + } + } catch { + // Fall through with the original client — covers test fakes. + } + + try { + const order = await bundledClient.createOrder(args.service, args.country); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + ...baseResponse, + status: 'paid_and_bought', + order: { + order_id: order.order_id, + phone_number: order.phone_number, + service: order.service ?? args.service, + country: order.country ?? args.country, + status: order.status, + expires_at: order.expires_at, + }, + tip: 'Use wait_for_sms with this order_id to collect the SMS code.', + }, + null, + 2 + ), + }, + ], + }; + } catch (err) { + // Topup succeeded but the bundled buy failed. Don't lose the api_key. + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + ...baseResponse, + status: 'paid_buy_failed', + buy_error: (err as Error).message, + tip: 'Topup succeeded — your api_key is bound to a $' + result.balance_usd.toFixed(2) + ' balance. Retry the purchase via create_order with the api_key above.', + }, + null, + 2 + ), + }, + ], + }; + } +} diff --git a/src/tools/v1_3/subscribe-webhook.ts b/src/tools/v1_3/subscribe-webhook.ts new file mode 100644 index 0000000..cc44bb5 --- /dev/null +++ b/src/tools/v1_3/subscribe-webhook.ts @@ -0,0 +1,129 @@ +/** + * virtualsms_subscribe_webhook — outbound event delivery subscription. + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.6 and docs/v1.3.0-plan.md Task 8. + * + * Backend support: POST /api/v1/customer/webhooks (already shipped). + */ + +import { z } from 'zod'; +import type { VirtualSMSClient } from '../../client.js'; + +export const ALLOWED_WEBHOOK_EVENTS = [ + 'sms.received', + 'order.cancelled', + 'order.expired', + 'order.swapped', + 'balance.low', +] as const; + +export const SubscribeWebhookInput = z.object({ + url: z + .string() + .url() + .describe('HTTPS-only callback URL — backend rejects http://'), + events: z + .array(z.enum(ALLOWED_WEBHOOK_EVENTS)) + .min(1) + .describe('1+ events from: sms.received, order.cancelled, order.expired, order.swapped, balance.low'), + threshold_usd: z + .number() + .optional() + .describe('Required if events includes "balance.low" — fire when balance drops below this'), + description: z.string().optional().describe('Free-form label for the webhook'), +}); + +export const SUBSCRIBE_WEBHOOK_TOOL_DEF = { + name: 'virtualsms_subscribe_webhook', + title: 'Subscribe to Webhook Events', + description: + 'Subscribe to outbound webhook deliveries for sms.received, order.cancelled, order.expired, ' + + 'order.swapped, or balance.low events. Returns webhook_id and signing secret. ' + + 'Use this for long-running agents instead of polling. ' + + 'For balance.low, threshold_usd is required.', + inputSchema: { + type: 'object' as const, + properties: { + url: { type: 'string', format: 'uri' }, + events: { + type: 'array', + items: { + type: 'string', + enum: ['sms.received', 'order.cancelled', 'order.expired', 'order.swapped', 'balance.low'], + }, + minItems: 1, + }, + threshold_usd: { type: 'number' }, + description: { type: 'string' }, + }, + required: ['url', 'events'], + }, + annotations: { + title: 'Subscribe to Webhook Events', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, +}; + +export async function handleSubscribeWebhook( + client: VirtualSMSClient, + args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + // Pre-flight: balance.low requires threshold_usd. Same pattern as the + // cooldown pre-check in v1.2.3 — fail fast client-side, save a 4xx. + if (args.events.includes('balance.low') && (typeof args.threshold_usd !== 'number' || args.threshold_usd <= 0)) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'threshold_required', + message: + 'balance.low webhook requires a positive threshold_usd. The webhook fires when account balance drops below this value.', + tip: 'Pass e.g. threshold_usd: 5 to be alerted when balance < $5.', + }, + null, + 2 + ), + }, + ], + }; + } + + const wh = await client.createWebhook({ + url: args.url, + events: args.events, + threshold_usd: args.threshold_usd, + description: args.description, + }); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + webhook_id: wh.id, + url: wh.url, + events: wh.events, + secret: wh.secret, + active: wh.active ?? true, + created_at: wh.created_at, + tip: + 'Verify deliveries with HMAC-SHA256 of the request body using `secret`. ' + + 'See https://docs.virtualsms.io/webhooks. ' + + 'Use manage_webhooks(action:"test", webhook_id:"' + + wh.id + + '") to fire a test event right now.', + }, + null, + 2 + ), + }, + ], + }; +} diff --git a/src/tools/v1_3/wait-batch.ts b/src/tools/v1_3/wait-batch.ts new file mode 100644 index 0000000..228debf --- /dev/null +++ b/src/tools/v1_3/wait-batch.ts @@ -0,0 +1,305 @@ +/** + * virtualsms_wait_for_sms_batch — wait for SMS on N orders in parallel. + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.2 and docs/v1.3.0-plan.md Task 4. + */ + +import { z } from 'zod'; +import WebSocket from 'ws'; +import type { VirtualSMSClient, Order } from '../../client.js'; + +export const WaitForSmsBatchInput = z.object({ + order_ids: z + .array(z.string()) + .min(1) + .max(20) + .describe('Array of 1-20 order IDs returned from buy_batch or create_order'), + timeout_seconds: z + .number() + .int() + .min(5) + .max(600) + .default(120) + .describe('Per-order timeout in seconds (default 120)'), + return_partial: z + .boolean() + .default(true) + .describe('Return what arrived even if some timed out (default true)'), +}); + +export const WAIT_FOR_SMS_BATCH_TOOL_DEF = { + name: 'virtualsms_wait_for_sms_batch', + title: 'Wait for SMS on Batch', + description: + 'Wait for SMS verification codes to arrive on N orders in parallel. ' + + 'Uses WebSocket for each order with polling fallback. ' + + 'Returns received[], timed_out[], and errors[] arrays. ' + + 'Pair with buy_batch for the canonical batch agentic pattern.', + inputSchema: { + type: 'object' as const, + properties: { + order_ids: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 20 }, + timeout_seconds: { type: 'number', minimum: 5, maximum: 600, default: 120 }, + return_partial: { type: 'boolean', default: true }, + }, + required: ['order_ids'], + }, + annotations: { + title: 'Wait for SMS on Batch', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, +}; + +function extractCode(text: string): string | undefined { + if (!text) return undefined; + const m = text.match(/\b(\d{4,8})\b/); + return m ? m[1] : undefined; +} + +interface PerOrderResult { + order_id: string; + status: 'received' | 'timed_out' | 'errored'; + code?: string; + sms_text?: string; + delivery_method?: 'websocket' | 'polling' | 'instant'; + error?: string; + elapsed_ms?: number; +} + +/** + * Wait for SMS on a single order, racing WebSocket against polling, with a + * shared deadline. Returns a structured per-order result. Never throws. + */ +async function waitForOneOrder( + client: VirtualSMSClient, + orderId: string, + timeoutMs: number +): Promise { + const start = Date.now(); + const apiKey = client.getApiKey(); + const baseUrl = client.getBaseUrl(); + + // Short-circuit on the first poll. + let initial: Order | undefined; + try { + initial = await client.getOrder(orderId); + } catch (err) { + return { order_id: orderId, status: 'errored', error: (err as Error).message }; + } + const messages = initial.messages ?? []; + if (messages.length > 0 || initial.sms_code || initial.sms_text) { + const text = messages[0]?.content || initial.sms_text || initial.sms_code || ''; + return { + order_id: orderId, + status: 'received', + code: initial.sms_code || extractCode(text), + sms_text: text, + delivery_method: 'instant', + elapsed_ms: Date.now() - start, + }; + } + + const remaining = () => Math.max(0, timeoutMs - (Date.now() - start)); + + // WebSocket race (only if api key present) + let wsResolved = false; + const wsPromise = new Promise((resolve) => { + if (!apiKey) { + resolve(null); + return; + } + const wsUrl = + baseUrl.replace(/^http/, 'ws') + + `/ws/orders?order_id=${encodeURIComponent(orderId)}&api_key=${encodeURIComponent(apiKey)}`; + let ws: WebSocket | null = null; + const timer = setTimeout(() => { + if (!wsResolved) { + wsResolved = true; + ws?.close(); + resolve(null); + } + }, remaining()); + try { + ws = new WebSocket(wsUrl); + ws.on('error', () => { + if (!wsResolved) { + wsResolved = true; + clearTimeout(timer); + resolve(null); + } + }); + ws.on('close', () => { + if (!wsResolved) { + wsResolved = true; + clearTimeout(timer); + resolve(null); + } + }); + ws.on('message', (data: Buffer) => { + try { + const msg = JSON.parse(data.toString()); + if ((msg.type === 'sms' || msg.type === 'sms_received') && msg.code) { + if (!wsResolved) { + wsResolved = true; + clearTimeout(timer); + ws?.close(); + const text = msg.full_text || msg.message || ''; + resolve({ + order_id: orderId, + status: 'received', + code: String(msg.code), + sms_text: text, + delivery_method: 'websocket', + elapsed_ms: Date.now() - start, + }); + } + } + } catch { + // ignore parse errors + } + }); + } catch { + if (!wsResolved) { + wsResolved = true; + clearTimeout(timer); + resolve(null); + } + } + }); + + // Polling race + const pollPromise = (async (): Promise => { + const intervalMs = 5000; + while (remaining() > 0) { + await new Promise((r) => setTimeout(r, Math.min(intervalMs, remaining()))); + if (remaining() <= 0) break; + try { + const status = await client.getOrder(orderId); + const m = status.messages ?? []; + if (m.length > 0 || status.sms_code || status.sms_text) { + const text = m[0]?.content || status.sms_text || status.sms_code || ''; + return { + order_id: orderId, + status: 'received', + code: status.sms_code || extractCode(text), + sms_text: text, + delivery_method: 'polling', + elapsed_ms: Date.now() - start, + }; + } + if (status.status === 'cancelled' || status.status === 'failed') { + return { + order_id: orderId, + status: 'errored', + error: `Order ${orderId} was ${status.status} before SMS arrived.`, + }; + } + } catch (err) { + const message = (err as Error).message; + // Real errors (404 etc.) → bail + if (!message.includes('waiting') && !message.includes('pending')) { + return { order_id: orderId, status: 'errored', error: message }; + } + } + } + return { order_id: orderId, status: 'timed_out', elapsed_ms: Date.now() - start }; + })(); + + const winner = await Promise.race([ + wsPromise.then((r) => r ?? null), + pollPromise, + ]); + if (winner && (winner as PerOrderResult).status === 'received') { + return winner as PerOrderResult; + } + // WS resolved null (failed/timed out) → fall through to polling result. + return await pollPromise; +} + +export async function handleWaitForSmsBatch( + client: VirtualSMSClient, + args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + const timeoutMs = args.timeout_seconds * 1000; + const start = Date.now(); + + const settled = await Promise.allSettled( + args.order_ids.map((id) => waitForOneOrder(client, id, timeoutMs)) + ); + + const received: Array> = []; + const timedOut: string[] = []; + const errors: Array<{ order_id: string; error: string }> = []; + + settled.forEach((r, i) => { + const id = args.order_ids[i]; + if (r.status === 'fulfilled') { + const v = r.value; + if (v.status === 'received') { + received.push({ + order_id: v.order_id, + code: v.code, + sms_text: v.sms_text, + delivery_method: v.delivery_method, + elapsed_ms: v.elapsed_ms, + }); + } else if (v.status === 'timed_out') { + timedOut.push(v.order_id); + } else { + errors.push({ order_id: v.order_id, error: v.error ?? 'unknown' }); + } + } else { + errors.push({ order_id: id, error: (r.reason as Error)?.message ?? String(r.reason) }); + } + }); + + if (!args.return_partial && (timedOut.length > 0 || errors.length > 0)) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'partial_batch', + message: 'return_partial=false and not all orders delivered.', + received, + timed_out: timedOut, + errors, + elapsed_seconds: Math.round((Date.now() - start) / 1000), + }, + null, + 2 + ), + }, + ], + }; + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + received, + timed_out: timedOut, + errors, + elapsed_seconds: Math.round((Date.now() - start) / 1000), + tip: + timedOut.length > 0 + ? 'Some orders timed out — call get_sms with each timed_out id later, or cancel_order to refund.' + : received.length === args.order_ids.length + ? 'All SMS delivered.' + : undefined, + }, + null, + 2 + ), + }, + ], + }; +} diff --git a/src/tools/v1_3/x402-info.ts b/src/tools/v1_3/x402-info.ts new file mode 100644 index 0000000..a08fe5f --- /dev/null +++ b/src/tools/v1_3/x402-info.ts @@ -0,0 +1,110 @@ +/** + * virtualsms_x402_info — discover money-path capabilities (no payment required). + * + * STATUS: v1.3.0 STUB — signatures only, no implementation logic. + * See docs/v1.3.0-design.md §4.5 and docs/v1.3.0-plan.md Task 6. + */ + +import { z } from 'zod'; +import type { VirtualSMSClient } from '../../client.js'; + +export const X402InfoInput = z.object({}); + +export const X402_INFO_TOOL_DEF = { + name: 'virtualsms_x402_info', + title: 'x402 Capability Discovery', + description: + 'Discover whether this server accepts x402 payments and on which networks/assets. ' + + 'No payment required. Returns enabled flag, accepted networks (Base/Solana), assets ' + + '(USDC/USDT), and min/max amounts. Call before pay_and_buy.', + inputSchema: { + type: 'object' as const, + properties: {}, + required: [], + }, + annotations: { + title: 'x402 Capability Discovery', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, +}; + +// Networks that are intentionally NOT exposed via x402_info even if the +// backend somehow returns them. BNB / BSC don't support EIP-3009, our +// settler doesn't ship Permit2 yet — the upstream Vault memory +// `project_x402_wallet_setup` says: "Don't re-enable BNB without shipping +// Permit2 first." +const DISABLED_NETWORKS = new Set(['bsc', 'binance', 'bnb']); + +export async function handleX402Info( + client: VirtualSMSClient, + _args: z.infer +): Promise<{ content: Array<{ type: 'text'; text: string }> }> { + let info; + try { + info = await client.getX402Info(); + } catch (err) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + error: 'unsupported_on_this_backend', + enabled: false, + message: `This server doesn't expose /api/v1/x402/info: ${(err as Error).message}`, + tip: 'Self-hosted older API servers may not have x402 enabled. Contact the operator.', + }, + null, + 2 + ), + }, + ], + }; + } + + // Map raw networks → agent-friendly accepts[], filtering disabled networks. + const accepts = info.networks + .filter((n) => !DISABLED_NETWORKS.has(n.network.toLowerCase())) + .map((n) => ({ + network: n.network, + asset: n.token, + min_usd: info.min_topup_usd, + max_usd: info.max_topup_usd, + pay_to: n.network.toLowerCase() === 'solana' ? info.solana_relayer : info.evm_relayer, + })); + + // patterns: derived from what the backend exposes. + const patterns: string[] = []; + if (info.topup_endpoint) patterns.push('topup'); + // Pattern A is currently 410-deprecated server-side, but if a self-hosted + // server still surfaces `resource`, advertise it. + if (info.resource) patterns.push('sms-verify'); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + enabled: Boolean(info.enabled), + x402_version: info.x402_version, + patterns, + accepts, + min_topup_usd: info.min_topup_usd, + max_topup_usd: info.max_topup_usd, + default_topup_usd: info.default_topup_usd, + tip: + info.enabled && accepts.length > 0 + ? 'Use pay_and_buy to deposit (and optionally buy a number) in one shot.' + : 'x402 not enabled on this server. Use the dashboard topup at virtualsms.io/dashboard.', + }, + null, + 2 + ), + }, + ], + }; +} diff --git a/tests/__snapshots__/v1_2_3_schema_snapshot.test.ts.snap b/tests/__snapshots__/v1_2_3_schema_snapshot.test.ts.snap new file mode 100644 index 0000000..d5ae10c --- /dev/null +++ b/tests/__snapshots__/v1_2_3_schema_snapshot.test.ts.snap @@ -0,0 +1,568 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`v1.2.x backward-compat schema snapshot > annotations for every v1.2.x tool match the locked snapshot 1`] = ` +[ + { + "annotations": { + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Cancel All Active Orders", + }, + "name": "virtualsms_cancel_all_orders", + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Cancel Order", + }, + "name": "virtualsms_cancel_order", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Buy Virtual Number", + }, + "name": "virtualsms_create_order", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Find Cheapest Countries", + }, + "name": "virtualsms_find_cheapest", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Get Account Balance", + }, + "name": "virtualsms_get_balance", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Get Order Details", + }, + "name": "virtualsms_get_order", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Check Service Price", + }, + "name": "virtualsms_get_price", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Get Account Profile", + }, + "name": "virtualsms_get_profile", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Check SMS Code", + }, + "name": "virtualsms_get_sms", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Get Account Stats", + }, + "name": "virtualsms_get_stats", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Get Transaction History", + }, + "name": "virtualsms_get_transactions", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "List Available Countries", + }, + "name": "virtualsms_list_countries", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "List Active Orders", + }, + "name": "virtualsms_list_orders", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "List Available Services", + }, + "name": "virtualsms_list_services", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Order History", + }, + "name": "virtualsms_order_history", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Search Service by Name", + }, + "name": "virtualsms_search_services", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Swap Phone Number", + }, + "name": "virtualsms_swap_number", + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "Wait for SMS on Existing Order", + }, + "name": "virtualsms_wait_for_sms", + }, +] +`; + +exports[`v1.2.x backward-compat schema snapshot > description prefix is stable for every v1.2.x tool (catches accidental rewrites) 1`] = ` +[ + { + "description_head": "Bulk-cancel every currently active order in your account. Re", + "name": "virtualsms_cancel_all_orders", + }, + { + "description_head": "Cancel an order and request a refund. Only works if no SMS h", + "name": "virtualsms_cancel_order", + }, + { + "description_head": "Purchase a virtual phone number for SMS verification. Return", + "name": "virtualsms_create_order", + }, + { + "description_head": "Find the cheapest countries for a given service, sorted by p", + "name": "virtualsms_find_cheapest", + }, + { + "description_head": "Check your VirtualSMS account balance in USD. Requires VIRTU", + "name": "virtualsms_get_balance", + }, + { + "description_head": "Get the full details of a specific order, including status, ", + "name": "virtualsms_get_order", + }, + { + "description_head": "Check the price and availability for a specific service + co", + "name": "virtualsms_get_price", + }, + { + "description_head": "Full account profile: email, Telegram link status, current b", + "name": "virtualsms_get_profile", + }, + { + "description_head": "Check if an SMS verification code has been received for an o", + "name": "virtualsms_get_sms", + }, + { + "description_head": "Account usage stats aggregated from your order history: tota", + "name": "virtualsms_get_stats", + }, + { + "description_head": "Transaction history for the account with optional filters fo", + "name": "virtualsms_get_transactions", + }, + { + "description_head": "Get all available countries for SMS verification. Use this t", + "name": "virtualsms_list_countries", + }, + { + "description_head": "List your active orders. Essential for crash recovery — if y", + "name": "virtualsms_list_orders", + }, + { + "description_head": "Get all available SMS verification services (Telegram, Whats", + "name": "virtualsms_list_services", + }, + { + "description_head": "List past orders with optional filters for status, service, ", + "name": "virtualsms_order_history", + }, + { + "description_head": "Find the right service code using natural language. Don't kn", + "name": "virtualsms_search_services", + }, + { + "description_head": "Swap a phone number on an existing order. Gets a new number ", + "name": "virtualsms_swap_number", + }, + { + "description_head": "Wait (block) until the SMS arrives on an existing order_id, ", + "name": "virtualsms_wait_for_sms", + }, +] +`; + +exports[`v1.2.x backward-compat schema snapshot > inputSchema for every v1.2.x tool matches the locked snapshot 1`] = ` +[ + { + "inputSchema": { + "properties": {}, + "required": [], + "type": "object", + }, + "name": "virtualsms_cancel_all_orders", + }, + { + "inputSchema": { + "properties": { + "order_id": { + "description": "Order ID to cancel", + "type": "string", + }, + }, + "required": [ + "order_id", + ], + "type": "object", + }, + "name": "virtualsms_cancel_order", + }, + { + "inputSchema": { + "properties": { + "country": { + "description": "Country ISO code (e.g. "US", "GB", "RU")", + "type": "string", + }, + "service": { + "description": "Service code (e.g. "telegram", "whatsapp", "google")", + "type": "string", + }, + }, + "required": [ + "service", + "country", + ], + "type": "object", + }, + "name": "virtualsms_create_order", + }, + { + "inputSchema": { + "properties": { + "limit": { + "default": 5, + "description": "Number of cheapest options to return (default: 5)", + "type": "number", + }, + "service": { + "description": "Service code (e.g. "telegram", "whatsapp", "google")", + "type": "string", + }, + }, + "required": [ + "service", + ], + "type": "object", + }, + "name": "virtualsms_find_cheapest", + }, + { + "inputSchema": { + "properties": { + "currency": { + "description": "Display balance in specific currency (default: USD)", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_get_balance", + }, + { + "inputSchema": { + "properties": { + "order_id": { + "description": "Order ID to retrieve full details for", + "type": "string", + }, + }, + "required": [ + "order_id", + ], + "type": "object", + }, + "name": "virtualsms_get_order", + }, + { + "inputSchema": { + "properties": { + "country": { + "description": "Country ISO code (e.g. "US", "GB", "RU")", + "type": "string", + }, + "service": { + "description": "Service code (e.g. "telegram", "whatsapp", "google")", + "type": "string", + }, + }, + "required": [ + "service", + "country", + ], + "type": "object", + }, + "name": "virtualsms_get_price", + }, + { + "inputSchema": { + "properties": {}, + "required": [], + "type": "object", + }, + "name": "virtualsms_get_profile", + }, + { + "inputSchema": { + "properties": { + "order_id": { + "description": "Order ID returned from buy_number", + "type": "string", + }, + }, + "required": [ + "order_id", + ], + "type": "object", + }, + "name": "virtualsms_get_sms", + }, + { + "inputSchema": { + "properties": { + "since_days": { + "default": 30, + "description": "Window in days for activity stats (default: 30)", + "type": "number", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_get_stats", + }, + { + "inputSchema": { + "properties": { + "from": { + "description": "Lower bound on created_at — RFC3339 or YYYY-MM-DD", + "type": "string", + }, + "limit": { + "default": 50, + "description": "Max transactions (1-200, default: 50)", + "type": "number", + }, + "offset": { + "default": 0, + "description": "Pagination offset (default: 0)", + "type": "number", + }, + "to": { + "description": "Upper bound on created_at — RFC3339 or YYYY-MM-DD", + "type": "string", + }, + "type": { + "description": "Filter by type: "deposit", "purchase", "refund", "admin_credit"", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_get_transactions", + }, + { + "inputSchema": { + "properties": { + "service": { + "description": "Filter countries available for a specific service (optional)", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_list_countries", + }, + { + "inputSchema": { + "properties": { + "status": { + "description": "Optional status filter: "pending", "sms_received", "cancelled", "completed"", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_list_orders", + }, + { + "inputSchema": { + "properties": { + "search": { + "description": "Filter services by name (optional)", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_list_services", + }, + { + "inputSchema": { + "properties": { + "country": { + "description": "Optional country ISO code filter (e.g. "US", "GB")", + "type": "string", + }, + "limit": { + "default": 20, + "description": "Max orders to return (default: 20, server cap: 50)", + "type": "number", + }, + "service": { + "description": "Optional service code filter (e.g. "telegram", "whatsapp")", + "type": "string", + }, + "since_days": { + "description": "Only include orders from the last N days", + "type": "number", + }, + "status": { + "description": "Optional status filter: "completed", "cancelled", "expired", "sms_received", "waiting"", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + "name": "virtualsms_order_history", + }, + { + "inputSchema": { + "properties": { + "query": { + "description": "Natural language search query (e.g. "uber", "whatsapp", "binance")", + "type": "string", + }, + }, + "required": [ + "query", + ], + "type": "object", + }, + "name": "virtualsms_search_services", + }, + { + "inputSchema": { + "properties": { + "order_id": { + "description": "Order ID to swap — must be in waiting/created status with no SMS received", + "type": "string", + }, + }, + "required": [ + "order_id", + ], + "type": "object", + }, + "name": "virtualsms_swap_number", + }, + { + "inputSchema": { + "properties": { + "order_id": { + "description": "Existing order ID returned from create_order", + "type": "string", + }, + "timeout_seconds": { + "default": 60, + "description": "How long to wait for SMS in seconds (default: 60, min: 5, max: 600)", + "maximum": 600, + "minimum": 5, + "type": "number", + }, + }, + "required": [ + "order_id", + ], + "type": "object", + }, + "name": "virtualsms_wait_for_sms", + }, +] +`; diff --git a/tests/balance-hint.test.ts b/tests/balance-hint.test.ts new file mode 100644 index 0000000..4a58a53 --- /dev/null +++ b/tests/balance-hint.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleGetBalance } from '../src/tools.js'; +import type { VirtualSMSClient, X402Info } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +describe('handleGetBalance — v1.3.0 additive fields', () => { + test('still returns balance_usd (backward compat)', async () => { + const client = makeClient({ + getBalance: vi.fn(async () => ({ balance_usd: 12.4 })), + getX402Info: vi.fn(async () => ({ enabled: true, networks: [] }) as X402Info), + }); + const out = await handleGetBalance(client); + const data = parseResult(out); + expect(data.balance_usd).toBe(12.4); + }); + + test('adds topup_url + x402_topup_available when x402 enabled', async () => { + const client = makeClient({ + getBalance: vi.fn(async () => ({ balance_usd: 1.5 })), + getX402Info: vi.fn(async () => ({ enabled: true, networks: [{ network: 'base', token: 'USDC' }] }) as X402Info), + }); + const out = await handleGetBalance(client); + const data = parseResult(out); + expect(typeof data.topup_url).toBe('string'); + expect((data.topup_url as string).startsWith('https://')).toBe(true); + expect(data.x402_topup_available).toBe(true); + }); + + test('x402 disabled → x402_topup_available=false but topup_url still present', async () => { + const client = makeClient({ + getBalance: vi.fn(async () => ({ balance_usd: 1.5 })), + getX402Info: vi.fn(async () => ({ enabled: false, networks: [] }) as X402Info), + }); + const out = await handleGetBalance(client); + const data = parseResult(out); + expect(data.x402_topup_available).toBe(false); + expect(typeof data.topup_url).toBe('string'); + }); + + test('x402 lookup fails → still returns balance, x402_topup_available=false', async () => { + const client = makeClient({ + getBalance: vi.fn(async () => ({ balance_usd: 0.5 })), + getX402Info: vi.fn(async () => { + throw new Error('503'); + }), + }); + const out = await handleGetBalance(client); + const data = parseResult(out); + expect(data.balance_usd).toBe(0.5); + expect(data.x402_topup_available).toBe(false); + }); +}); diff --git a/tests/buy-batch.test.ts b/tests/buy-batch.test.ts new file mode 100644 index 0000000..febb720 --- /dev/null +++ b/tests/buy-batch.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleBuyBatch, BuyBatchInput } from '../src/tools/v1_3/buy-batch.js'; +import type { VirtualSMSClient, BatchPurchaseResult, Balance, Price } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +describe('virtualsms_buy_batch', () => { + test('input validation rejects count outside 1-20', () => { + expect(() => BuyBatchInput.parse({ service: 'tg', country: 'GB', count: 0 })).toThrow(); + expect(() => BuyBatchInput.parse({ service: 'tg', country: 'GB', count: 21 })).toThrow(); + // Defaults + const ok = BuyBatchInput.parse({ service: 'tg', country: 'GB', count: 5 }); + expect(ok.stop_on_failure).toBe(false); + }); + + test('happy path returns succeeded[] with all order_ids', async () => { + const fake: BatchPurchaseResult = { + succeeded: [ + { index: 0, order_id: 'o1', phone_number: '+44...', price: 0.1 }, + { index: 1, order_id: 'o2', phone_number: '+44...', price: 0.1 }, + { index: 2, order_id: 'o3', phone_number: '+44...', price: 0.1 }, + ], + failed: [], + }; + const client = makeClient({ + getBalance: vi.fn(async (): Promise => ({ balance_usd: 5 })), + checkPrice: vi.fn(async (): Promise => ({ price_usd: 0.1, currency: 'USD', available: true })), + createOrderBatch: vi.fn(async () => fake), + }); + const out = await handleBuyBatch(client, BuyBatchInput.parse({ service: 'tg', country: 'GB', count: 3 })); + const data = parseResult(out); + expect(Array.isArray(data.succeeded)).toBe(true); + expect((data.succeeded as Array).length).toBe(3); + expect(data.failed).toEqual([]); + expect(typeof data.tip).toBe('string'); + }); + + test('partial failure returns succeeded + failed populated', async () => { + const fake: BatchPurchaseResult = { + succeeded: [ + { index: 0, order_id: 'o1', phone_number: '+44...', price: 0.1 }, + { index: 2, order_id: 'o3', phone_number: '+44...', price: 0.1 }, + ], + failed: [{ index: 1, error: 'Insufficient stock' }], + }; + const client = makeClient({ + getBalance: vi.fn(async (): Promise => ({ balance_usd: 5 })), + checkPrice: vi.fn(async (): Promise => ({ price_usd: 0.1, currency: 'USD', available: true })), + createOrderBatch: vi.fn(async () => fake), + }); + const out = await handleBuyBatch(client, BuyBatchInput.parse({ service: 'tg', country: 'GB', count: 3 })); + const data = parseResult(out); + expect((data.succeeded as Array).length).toBe(2); + expect((data.failed as Array).length).toBe(1); + expect((data.failed as Array<{ error: string }>)[0].error).toMatch(/Insufficient stock/); + }); + + test('refuses when balance × cheapest_price guard would deplete > 80%', async () => { + // Balance $0.5, price $0.1 each, count 5 → would spend $0.5 (100% of balance). + // 5 × 0.1 = 0.5 > 0.5 × 0.8 = 0.4. Should refuse. + const client = makeClient({ + getBalance: vi.fn(async (): Promise => ({ balance_usd: 0.5 })), + checkPrice: vi.fn(async (): Promise => ({ price_usd: 0.1, currency: 'USD', available: true })), + createOrderBatch: vi.fn(async () => ({ succeeded: [], failed: [] })), + }); + const out = await handleBuyBatch(client, BuyBatchInput.parse({ service: 'tg', country: 'GB', count: 5 })); + const data = parseResult(out); + expect(data.error).toBe('budget_guard'); + // Did NOT call the batch endpoint. + expect((client.createOrderBatch as ReturnType)).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/buy-hint.test.ts b/tests/buy-hint.test.ts new file mode 100644 index 0000000..4ee6754 --- /dev/null +++ b/tests/buy-hint.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { handleBuyNumber, BuyNumberInput, _resetWebhookCacheForTests } from '../src/tools.js'; +import type { VirtualSMSClient, Order, Webhook } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +const fakeOrder: Order = { + order_id: 'o1', + phone_number: '+44...', + status: 'waiting', +}; + +beforeEach(() => { + _resetWebhookCacheForTests(); +}); + +describe('handleBuyNumber — v1.3.0 additive webhook hint', () => { + test('still returns order_id + phone_number (backward compat)', async () => { + const client = makeClient({ + createOrder: vi.fn(async () => fakeOrder), + listWebhooks: vi.fn(async () => []), + }); + const out = await handleBuyNumber(client, BuyNumberInput.parse({ service: 'tg', country: 'GB' })); + const data = parseResult(out); + expect(data.order_id).toBe('o1'); + expect(data.phone_number).toBe('+44...'); + }); + + test('no webhook for sms.received → adds webhook_subscribe_hint tip', async () => { + const client = makeClient({ + createOrder: vi.fn(async () => fakeOrder), + listWebhooks: vi.fn(async () => [] as Webhook[]), + }); + const out = await handleBuyNumber(client, BuyNumberInput.parse({ service: 'tg', country: 'GB' })); + const data = parseResult(out); + expect(data.webhook_subscribe_hint).toBeDefined(); + expect(typeof data.webhook_subscribe_hint).toBe('string'); + expect(data.webhook_subscribe_hint as string).toMatch(/subscribe_webhook/); + }); + + test('existing sms.received webhook → suppresses hint', async () => { + const client = makeClient({ + createOrder: vi.fn(async () => fakeOrder), + listWebhooks: vi.fn(async () => [ + { id: 'wh_1', url: 'https://x.test', events: ['sms.received'], active: true }, + ] as Webhook[]), + }); + const out = await handleBuyNumber(client, BuyNumberInput.parse({ service: 'tg', country: 'GB' })); + const data = parseResult(out); + expect(data.webhook_subscribe_hint).toBeUndefined(); + }); + + test('cache: 2nd call within 30s does not re-query listWebhooks', async () => { + const list = vi.fn(async () => [] as Webhook[]); + const client = makeClient({ + createOrder: vi.fn(async () => fakeOrder), + listWebhooks: list, + }); + await handleBuyNumber(client, BuyNumberInput.parse({ service: 'tg', country: 'GB' })); + await handleBuyNumber(client, BuyNumberInput.parse({ service: 'tg', country: 'GB' })); + expect(list).toHaveBeenCalledTimes(1); + }); + + test('listWebhooks fails → still returns order, suppresses hint', async () => { + const client = makeClient({ + createOrder: vi.fn(async () => fakeOrder), + listWebhooks: vi.fn(async () => { + throw new Error('lookup failed'); + }), + }); + const out = await handleBuyNumber(client, BuyNumberInput.parse({ service: 'tg', country: 'GB' })); + const data = parseResult(out); + expect(data.order_id).toBe('o1'); + expect(data.webhook_subscribe_hint).toBeUndefined(); + }); +}); diff --git a/tests/client-v1_3-methods.test.ts b/tests/client-v1_3-methods.test.ts new file mode 100644 index 0000000..c0aa2ca --- /dev/null +++ b/tests/client-v1_3-methods.test.ts @@ -0,0 +1,209 @@ +/** + * Backend client method tests for v1.3.0 additions. + * + * Per docs/v1.3.0-plan.md Task 2: x402 + webhooks methods. + * + * Strategy: instantiate the client and stub the internal axios instance to + * intercept GET/POST/DELETE calls. We assert (1) the right URL is hit, + * (2) the right method is used, (3) the right payload is sent, and + * (4) the response is shape-mapped. + */ + +import { describe, expect, test, vi } from 'vitest'; +import { VirtualSMSClient } from '../src/client.js'; + +interface AxiosLike { + get: ReturnType; + post: ReturnType; + delete: ReturnType; + interceptors: { request: { use: () => void }; response: { use: () => void } }; +} + +function patchClientHttp(client: VirtualSMSClient): AxiosLike { + const fake: AxiosLike = { + get: vi.fn(), + post: vi.fn(), + delete: vi.fn(), + interceptors: { request: { use: () => undefined }, response: { use: () => undefined } }, + }; + // The constructor already created a real axios instance — replace it. + // Cast to any for the mutation; production code never does this. + (client as unknown as { http: AxiosLike }).http = fake; + return fake; +} + +describe('VirtualSMSClient v1.3.0 methods', () => { + describe('getX402Info', () => { + test('hits GET /api/v1/x402/info and shape-maps the response', async () => { + const client = new VirtualSMSClient('https://example.com'); + const fake = patchClientHttp(client); + fake.get.mockResolvedValueOnce({ + data: { + enabled: true, + x402_version: 1, + networks: [ + { network: 'solana', token: 'USDC' }, + { network: 'base', token: 'USDC' }, + ], + evm_relayer: '0xfEc54264350d97d9b63f9Cc415BAF708C4695F32', + solana_relayer: '7AJwx3J2qXnURXZmU5AotDeMUY5dDBqBFbweHLZ2UeUs', + min_topup_usd: 2, + max_topup_usd: 10000, + default_topup_usd: 5, + topup_endpoint: 'https://example.com/api/v1/x402/topup', + resource: 'https://example.com/api/v1/x402/sms-verify', + }, + }); + + const info = await client.getX402Info(); + expect(fake.get).toHaveBeenCalledWith('/api/v1/x402/info'); + expect(info.enabled).toBe(true); + expect(info.networks).toEqual([ + { network: 'solana', token: 'USDC' }, + { network: 'base', token: 'USDC' }, + ]); + expect(info.min_topup_usd).toBe(2); + expect(info.max_topup_usd).toBe(10000); + expect(info.topup_endpoint).toBe('https://example.com/api/v1/x402/topup'); + }); + + test('does NOT require api key (it is discovery)', async () => { + const client = new VirtualSMSClient('https://example.com'); // no api key + const fake = patchClientHttp(client); + fake.get.mockResolvedValueOnce({ data: { enabled: false } }); + // Must not throw + await expect(client.getX402Info()).resolves.toBeDefined(); + }); + }); + + describe('topup', () => { + test('without payment proof returns the 402 manifest payload', async () => { + const client = new VirtualSMSClient('https://example.com'); + const fake = patchClientHttp(client); + fake.post.mockResolvedValueOnce({ + data: { x402Version: 1, accepts: [{ scheme: 'exact', network: 'base' }], error: 'Payment required' }, + }); + const result = await client.topup({ amount_usd: 5, payment_method: 'usdc-base' }); + expect(fake.post).toHaveBeenCalledWith( + '/api/v1/x402/topup', + { amount_usd: 5, payment_method: 'usdc-base' }, + { headers: {} } + ); + expect((result as Record).x402Version).toBe(1); + }); + + test('with payment proof forwards X-PAYMENT header', async () => { + const client = new VirtualSMSClient('https://example.com'); + const fake = patchClientHttp(client); + fake.post.mockResolvedValueOnce({ + data: { api_key: 'vsms_abc', balance_usd: 5 }, + }); + await client.topup({ amount_usd: 5, payment_method: 'usdc-base', payment_proof: 'eyJ...' }); + expect(fake.post).toHaveBeenCalledWith( + '/api/v1/x402/topup', + { amount_usd: 5, payment_method: 'usdc-base' }, + { headers: { 'X-PAYMENT': 'eyJ...' } } + ); + }); + }); + + describe('webhooks CRUD', () => { + test('listWebhooks → GET /api/v1/customer/webhooks (requires api key)', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + fake.get.mockResolvedValueOnce({ data: { webhooks: [{ id: 'wh_1', url: 'https://x.test', events: ['sms.received'] }] } }); + const list = await client.listWebhooks(); + expect(fake.get).toHaveBeenCalledWith('/api/v1/customer/webhooks'); + expect(Array.isArray(list)).toBe(true); + expect(list[0].id).toBe('wh_1'); + }); + + test('listWebhooks throws without api key', async () => { + const client = new VirtualSMSClient('https://example.com'); // no key + // Don't patch — requireApiKey throws synchronously before axios is touched. + await expect(client.listWebhooks()).rejects.toThrow(/VIRTUALSMS_API_KEY/); + }); + + test('createWebhook → POST with url/events/threshold_usd/description', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + fake.post.mockResolvedValueOnce({ + data: { id: 'wh_1', url: 'https://x.test', events: ['sms.received'], secret: 'whsec_x', active: true }, + }); + const wh = await client.createWebhook({ + url: 'https://x.test', + events: ['sms.received', 'balance.low'], + threshold_usd: 5, + description: 'agent-fanout', + }); + expect(fake.post).toHaveBeenCalledWith('/api/v1/customer/webhooks', { + url: 'https://x.test', + events: ['sms.received', 'balance.low'], + threshold_usd: 5, + description: 'agent-fanout', + }); + expect(wh.id).toBe('wh_1'); + expect(wh.secret).toBe('whsec_x'); + }); + + test('deleteWebhook → DELETE /api/v1/customer/webhooks/:id', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + fake.delete.mockResolvedValueOnce({ data: { deleted: true } }); + const r = await client.deleteWebhook('wh_1'); + expect(fake.delete).toHaveBeenCalledWith('/api/v1/customer/webhooks/wh_1'); + expect(r.deleted).toBe(true); + }); + + test('testWebhook → POST /api/v1/customer/webhooks/:id/test', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + fake.post.mockResolvedValueOnce({ data: { delivered: true, response_code: 200 } }); + const r = await client.testWebhook('wh_1'); + expect(fake.post).toHaveBeenCalledWith('/api/v1/customer/webhooks/wh_1/test'); + expect(r.delivered).toBe(true); + }); + + test('getDeliveries → GET /api/v1/customer/webhooks/:id/deliveries', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + fake.get.mockResolvedValueOnce({ data: { deliveries: [{ id: 'd_1', status: 'delivered' }] } }); + const r = await client.getDeliveries('wh_1'); + expect(fake.get).toHaveBeenCalledWith('/api/v1/customer/webhooks/wh_1/deliveries'); + expect(r[0].id).toBe('d_1'); + }); + }); + + describe('createOrderBatch', () => { + test('fans out N parallel createOrder calls and returns aggregate', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + // 3 successes, then one failure on the 4th + fake.post + .mockResolvedValueOnce({ data: { order_id: 'o1', phone_number: '+44...', status: 'waiting' } }) + .mockResolvedValueOnce({ data: { order_id: 'o2', phone_number: '+44...', status: 'waiting' } }) + .mockResolvedValueOnce({ data: { order_id: 'o3', phone_number: '+44...', status: 'waiting' } }); + + const batch = await client.createOrderBatch('telegram', 'GB', 3); + expect(fake.post).toHaveBeenCalledTimes(3); + expect(fake.post).toHaveBeenCalledWith('/api/v1/customer/purchase', { service: 'telegram', country: 'GB' }); + expect(batch.succeeded.length).toBe(3); + expect(batch.failed.length).toBe(0); + }); + + test('aggregates failures into failed[] without throwing', async () => { + const client = new VirtualSMSClient('https://example.com', 'vsms_test'); + const fake = patchClientHttp(client); + fake.post + .mockResolvedValueOnce({ data: { order_id: 'o1', phone_number: '+44...', status: 'waiting' } }) + .mockRejectedValueOnce(new Error('Insufficient balance')) + .mockResolvedValueOnce({ data: { order_id: 'o3', phone_number: '+44...', status: 'waiting' } }); + + const batch = await client.createOrderBatch('telegram', 'GB', 3); + expect(batch.succeeded.length).toBe(2); + expect(batch.failed.length).toBe(1); + expect(batch.failed[0].index).toBe(1); + expect(batch.failed[0].error).toMatch(/Insufficient balance/); + }); + }); +}); diff --git a/tests/find-best-pick.test.ts b/tests/find-best-pick.test.ts new file mode 100644 index 0000000..cb5e471 --- /dev/null +++ b/tests/find-best-pick.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleFindBestPick, FindBestPickInput } from '../src/tools/v1_3/find-best-pick.js'; +import type { VirtualSMSClient, Country, Price } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +const ALL_COUNTRIES: Country[] = [ + { iso: 'US', name: 'United States' }, + { iso: 'GB', name: 'United Kingdom' }, + { iso: 'DE', name: 'Germany' }, + { iso: 'NL', name: 'Netherlands' }, + { iso: 'RU', name: 'Russia' }, +]; + +const PRICE_TABLE: Record = { + US: { price_usd: 0.5, available: true }, + GB: { price_usd: 0.3, available: true }, + DE: { price_usd: 0.2, available: true }, + NL: { price_usd: 0.25, available: true }, + RU: { price_usd: 0.1, available: false }, // out of stock +}; + +function priceFor(_service: string, country: string): Promise { + const p = PRICE_TABLE[country]; + if (!p) return Promise.reject(new Error(`Not found ${country}`)); + return Promise.resolve({ price_usd: p.price_usd, currency: 'USD', available: p.available }); +} + +describe('virtualsms_find_best_pick', () => { + test('input validation — service required, prefer defaults to balanced', () => { + expect(() => FindBestPickInput.parse({})).toThrow(); + const ok = FindBestPickInput.parse({ service: 'tg' }); + expect(ok.prefer).toBe('balanced'); + }); + + test('cheapest mode picks the absolute cheapest with stock', async () => { + const client = makeClient({ + listCountries: vi.fn(async () => ALL_COUNTRIES), + checkPrice: vi.fn((s: string, c: string) => priceFor(s, c)), + }); + const out = await handleFindBestPick( + client, + FindBestPickInput.parse({ service: 'tg', prefer: 'cheapest' }) + ); + const data = parseResult(out); + const pick = data.pick as { country: string }; + // RU is cheaper but out of stock — DE is the cheapest WITH stock. + expect(pick.country).toBe('DE'); + }); + + test('country_pool restricts to whitelist', async () => { + const client = makeClient({ + listCountries: vi.fn(async () => ALL_COUNTRIES), + checkPrice: vi.fn((s: string, c: string) => priceFor(s, c)), + }); + const out = await handleFindBestPick( + client, + FindBestPickInput.parse({ service: 'tg', country_pool: ['US', 'GB'] }) + ); + const data = parseResult(out); + const pick = data.pick as { country: string }; + // Only US + GB considered → GB is cheaper. + expect(pick.country).toBe('GB'); + }); + + test('country_exclude blacklists countries', async () => { + const client = makeClient({ + listCountries: vi.fn(async () => ALL_COUNTRIES), + checkPrice: vi.fn((s: string, c: string) => priceFor(s, c)), + }); + const out = await handleFindBestPick( + client, + FindBestPickInput.parse({ service: 'tg', country_exclude: ['DE', 'RU'] }) + ); + const data = parseResult(out); + const pick = data.pick as { country: string }; + // DE excluded, RU OOS — NL is next cheapest at 0.25. + expect(pick.country).toBe('NL'); + }); + + test('reasoning is plain English with country name + price', async () => { + const client = makeClient({ + listCountries: vi.fn(async () => ALL_COUNTRIES), + checkPrice: vi.fn((s: string, c: string) => priceFor(s, c)), + }); + const out = await handleFindBestPick(client, FindBestPickInput.parse({ service: 'tg' })); + const data = parseResult(out); + expect(typeof data.reasoning).toBe('string'); + expect((data.reasoning as string).length).toBeGreaterThan(0); + const pick = data.pick as { country_name: string; country: string; price_usd: number }; + expect(pick.country_name).toBe('Germany'); + expect(pick.price_usd).toBe(0.2); + }); + + test('no countries available → returns no_pick error gracefully', async () => { + const client = makeClient({ + listCountries: vi.fn(async () => ALL_COUNTRIES), + checkPrice: vi.fn(async () => ({ price_usd: 0, currency: 'USD', available: false })), + }); + const out = await handleFindBestPick(client, FindBestPickInput.parse({ service: 'tg' })); + const data = parseResult(out); + expect(data.error).toBe('no_pick'); + }); +}); diff --git a/tests/manage-webhooks.test.ts b/tests/manage-webhooks.test.ts new file mode 100644 index 0000000..cb5ad6f --- /dev/null +++ b/tests/manage-webhooks.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleManageWebhooks, ManageWebhooksInput } from '../src/tools/v1_3/manage-webhooks.js'; +import type { VirtualSMSClient, Webhook, WebhookDelivery } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +describe('virtualsms_manage_webhooks', () => { + test('input validation — webhook_id required for delete/test/deliveries', () => { + expect(() => ManageWebhooksInput.parse({ action: 'delete' })).toThrow(); + expect(() => ManageWebhooksInput.parse({ action: 'test' })).toThrow(); + expect(() => ManageWebhooksInput.parse({ action: 'deliveries' })).toThrow(); + expect(() => ManageWebhooksInput.parse({ action: 'list' })).not.toThrow(); + expect(() => ManageWebhooksInput.parse({ action: 'delete', webhook_id: 'wh_1' })).not.toThrow(); + }); + + test('action=list dispatches to listWebhooks', async () => { + const fake: Webhook[] = [ + { id: 'wh_1', url: 'https://x.test', events: ['sms.received'], active: true }, + ]; + const list = vi.fn(async () => fake); + const client = makeClient({ listWebhooks: list }); + const out = await handleManageWebhooks( + client, + ManageWebhooksInput.parse({ action: 'list' }) + ); + const data = parseResult(out); + expect(list).toHaveBeenCalled(); + const wh = data.webhooks as Array<{ id: string }>; + expect(wh[0].id).toBe('wh_1'); + expect(data.count).toBe(1); + }); + + test('action=delete dispatches to deleteWebhook(id)', async () => { + const del = vi.fn(async () => ({ deleted: true })); + const client = makeClient({ deleteWebhook: del }); + const out = await handleManageWebhooks( + client, + ManageWebhooksInput.parse({ action: 'delete', webhook_id: 'wh_1' }) + ); + const data = parseResult(out); + expect(del).toHaveBeenCalledWith('wh_1'); + expect(data.deleted).toBe(true); + }); + + test('action=test dispatches to testWebhook(id)', async () => { + const test1 = vi.fn(async () => ({ delivered: true, response_code: 200 })); + const client = makeClient({ testWebhook: test1 }); + const out = await handleManageWebhooks( + client, + ManageWebhooksInput.parse({ action: 'test', webhook_id: 'wh_1' }) + ); + const data = parseResult(out); + expect(test1).toHaveBeenCalledWith('wh_1'); + expect(data.delivered).toBe(true); + expect(data.response_code).toBe(200); + }); + + test('action=deliveries dispatches to getDeliveries(id)', async () => { + const fake: WebhookDelivery[] = [ + { id: 'd_1', status: 'delivered', response_code: 200 }, + ]; + const get = vi.fn(async () => fake); + const client = makeClient({ getDeliveries: get }); + const out = await handleManageWebhooks( + client, + ManageWebhooksInput.parse({ action: 'deliveries', webhook_id: 'wh_1' }) + ); + const data = parseResult(out); + expect(get).toHaveBeenCalledWith('wh_1'); + const ds = data.deliveries as Array<{ id: string }>; + expect(ds[0].id).toBe('d_1'); + }); +}); diff --git a/tests/pay-and-buy.test.ts b/tests/pay-and-buy.test.ts new file mode 100644 index 0000000..fd94c93 --- /dev/null +++ b/tests/pay-and-buy.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handlePayAndBuy, PayAndBuyInput } from '../src/tools/v1_3/pay-and-buy.js'; +import type { VirtualSMSClient, X402TopupResult, X402Manifest, Order } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +describe('virtualsms_pay_and_buy', () => { + test('input validation — amount_usd 2..50, payment_method default usdc-base', () => { + expect(() => PayAndBuyInput.parse({ amount_usd: 1 })).toThrow(); + expect(() => PayAndBuyInput.parse({ amount_usd: 51 })).toThrow(); + const ok = PayAndBuyInput.parse({ amount_usd: 5 }); + expect(ok.payment_method).toBe('usdc-base'); + }); + + test('first call (no payment_proof) → returns 402 manifest', async () => { + const fakeManifest: X402Manifest = { + x402Version: 1, + accepts: [{ scheme: 'exact', network: 'base', token: 'USDC', payTo: '0xabc' }], + error: 'Payment required', + }; + const client = makeClient({ + topup: vi.fn(async () => fakeManifest), + }); + const out = await handlePayAndBuy(client, PayAndBuyInput.parse({ amount_usd: 5 })); + const data = parseResult(out); + expect(data.status).toBe('payment_required'); + expect(data.manifest).toEqual(fakeManifest); + expect(typeof data.tip).toBe('string'); + }); + + test('second call (with payment_proof) → returns paid + api_key', async () => { + const fakePaid: X402TopupResult = { + api_key: 'vsms_xxxxx', + balance_usd: 5, + user_id: 'u_1', + raw: { api_key: 'vsms_xxxxx', balance_usd: 5 }, + }; + const client = makeClient({ + topup: vi.fn(async () => fakePaid), + }); + const out = await handlePayAndBuy( + client, + PayAndBuyInput.parse({ amount_usd: 5, payment_proof: 'eyJ...proof...' }) + ); + const data = parseResult(out); + expect(data.status).toBe('paid'); + expect(data.api_key).toBe('vsms_xxxxx'); + expect(data.credited_balance_usd).toBe(5); + expect(data.next_action).toMatch(/VIRTUALSMS_API_KEY/); + }); + + test('paid + service+country → bundles createOrder via a freshly-keyed client', async () => { + // The handler creates a new client with the freshly-minted api_key for + // the bundled buy. We simulate that by exposing a clientFactory hook on + // the handler — but per the design the impl should use the same client + // class. We patch the topup result + spy on createOrder of THIS test + // client (the handler is expected to mutate or re-instantiate). + // + // Simpler test: we simulate by having the topup return api_key and the + // SAME mock client also expose createOrder. The impl is expected to + // call createOrder on a client built using the topped-up api_key. + const fakePaid: X402TopupResult = { + api_key: 'vsms_new', + balance_usd: 5, + raw: { api_key: 'vsms_new', balance_usd: 5 }, + }; + const order: Order = { + order_id: 'o1', + phone_number: '+44...', + status: 'waiting', + }; + // Don't expose getBaseUrl — the handler then keeps the existing client + // (which is a mock) for the bundled createOrder call. + const client = makeClient({ + topup: vi.fn(async () => fakePaid), + createOrder: vi.fn(async () => order), + }); + const out = await handlePayAndBuy( + client, + PayAndBuyInput.parse({ + amount_usd: 5, + payment_proof: 'eyJ...proof...', + service: 'tg', + country: 'GB', + }) + ); + const data = parseResult(out); + expect(data.status).toBe('paid_and_bought'); + expect(data.api_key).toBe('vsms_new'); + expect((data.order as { order_id: string }).order_id).toBe('o1'); + }); + + test('service set without country → returns input_error before any topup', async () => { + const topup = vi.fn(async () => ({ raw: {} }) as unknown as X402TopupResult); + const client = makeClient({ topup }); + const out = await handlePayAndBuy( + client, + PayAndBuyInput.parse({ amount_usd: 5, service: 'tg' }) + ); + const data = parseResult(out); + expect(data.error).toBe('input_error'); + expect(topup).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/subscribe-webhook.test.ts b/tests/subscribe-webhook.test.ts new file mode 100644 index 0000000..204c6d2 --- /dev/null +++ b/tests/subscribe-webhook.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleSubscribeWebhook, SubscribeWebhookInput } from '../src/tools/v1_3/subscribe-webhook.js'; +import type { VirtualSMSClient, Webhook } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +describe('virtualsms_subscribe_webhook', () => { + test('rejects non-https URLs at zod layer', () => { + expect(() => + SubscribeWebhookInput.parse({ url: 'not-a-url', events: ['sms.received'] }) + ).toThrow(); + }); + + test('rejects unknown events', () => { + expect(() => + SubscribeWebhookInput.parse({ url: 'https://x.test', events: ['random.event'] }) + ).toThrow(); + }); + + test('rejects empty events array', () => { + expect(() => + SubscribeWebhookInput.parse({ url: 'https://x.test', events: [] }) + ).toThrow(); + }); + + test('happy path → returns webhook_id + secret + active', async () => { + const fake: Webhook = { + id: 'wh_1', + url: 'https://x.test/hook', + events: ['sms.received'], + secret: 'whsec_xxx', + active: true, + created_at: '2026-04-30T12:00:00Z', + }; + const create = vi.fn(async () => fake); + const client = makeClient({ createWebhook: create }); + const out = await handleSubscribeWebhook( + client, + SubscribeWebhookInput.parse({ url: 'https://x.test/hook', events: ['sms.received'] }) + ); + const data = parseResult(out); + expect(data.webhook_id).toBe('wh_1'); + expect(data.secret).toBe('whsec_xxx'); + expect(data.active).toBe(true); + expect(create).toHaveBeenCalledWith({ + url: 'https://x.test/hook', + events: ['sms.received'], + threshold_usd: undefined, + description: undefined, + }); + }); + + test('balance.low requires threshold_usd', async () => { + const create = vi.fn(); + const client = makeClient({ createWebhook: create }); + const out = await handleSubscribeWebhook( + client, + SubscribeWebhookInput.parse({ + url: 'https://x.test', + events: ['balance.low'], + // threshold_usd omitted — handler should refuse before calling backend + }) + ); + const data = parseResult(out); + expect(data.error).toBe('threshold_required'); + expect(create).not.toHaveBeenCalled(); + }); + + test('balance.low with threshold_usd → passes through to backend', async () => { + const fake: Webhook = { + id: 'wh_2', + url: 'https://x.test', + events: ['balance.low'], + secret: 'whsec_y', + active: true, + }; + const create = vi.fn(async () => fake); + const client = makeClient({ createWebhook: create }); + const out = await handleSubscribeWebhook( + client, + SubscribeWebhookInput.parse({ + url: 'https://x.test', + events: ['balance.low'], + threshold_usd: 5, + }) + ); + const data = parseResult(out); + expect(data.webhook_id).toBe('wh_2'); + expect(create).toHaveBeenCalledWith({ + url: 'https://x.test', + events: ['balance.low'], + threshold_usd: 5, + description: undefined, + }); + }); +}); diff --git a/tests/v1_2_3_schema_snapshot.test.ts b/tests/v1_2_3_schema_snapshot.test.ts new file mode 100644 index 0000000..6f224f0 --- /dev/null +++ b/tests/v1_2_3_schema_snapshot.test.ts @@ -0,0 +1,92 @@ +/** + * Backward-compat snapshot test for the v1.2.x tool surface. + * + * Locks the name + inputSchema + annotations of every tool that shipped in + * v1.2.x. ANY change to one of these fields will break this test, which means + * the change broke a 1.2.x client contract. If a snapshot diff appears: + * + * 1. STOP. Do not regenerate the snapshot. + * 2. Diagnose the change — was it intentional or a regression? + * 3. If the field was intentionally removed/renamed/changed, the v1.3.x + * change is no longer "additive" — it's a breaking change. Either + * rebuild it as additive, or coordinate a major version bump. + * + * v1.3.x adds new tools but never modifies existing ones. The snapshot below + * pins the 18 tools shipped in v1.2.3. + */ + +import { describe, expect, test } from 'vitest'; +import { TOOL_DEFINITIONS } from '../src/tools.js'; + +// The 18 tools that shipped in v1.2.x. Sorted alphabetically for stability. +const V1_2_X_TOOL_NAMES = [ + 'virtualsms_cancel_all_orders', + 'virtualsms_cancel_order', + 'virtualsms_create_order', + 'virtualsms_find_cheapest', + 'virtualsms_get_balance', + 'virtualsms_get_order', + 'virtualsms_get_price', + 'virtualsms_get_profile', + 'virtualsms_get_sms', + 'virtualsms_get_stats', + 'virtualsms_get_transactions', + 'virtualsms_list_countries', + 'virtualsms_list_orders', + 'virtualsms_list_services', + 'virtualsms_order_history', + 'virtualsms_search_services', + 'virtualsms_swap_number', + 'virtualsms_wait_for_sms', +] as const; + +function toolByName(name: string) { + const t = TOOL_DEFINITIONS.find((d) => d.name === name); + if (!t) throw new Error(`Tool ${name} missing from TOOL_DEFINITIONS`); + return t; +} + +describe('v1.2.x backward-compat schema snapshot', () => { + test('all 18 v1.2.x tools are still registered', () => { + const registeredNames = TOOL_DEFINITIONS.map((t) => t.name).sort(); + for (const name of V1_2_X_TOOL_NAMES) { + expect(registeredNames).toContain(name); + } + }); + + test('inputSchema for every v1.2.x tool matches the locked snapshot', () => { + const snapshot = V1_2_X_TOOL_NAMES.map((name) => { + const t = toolByName(name); + return { + name: t.name, + inputSchema: t.inputSchema, + }; + }); + expect(snapshot).toMatchSnapshot(); + }); + + test('annotations for every v1.2.x tool match the locked snapshot', () => { + const snapshot = V1_2_X_TOOL_NAMES.map((name) => { + const t = toolByName(name); + return { + name: t.name, + annotations: t.annotations, + }; + }); + expect(snapshot).toMatchSnapshot(); + }); + + test('description prefix is stable for every v1.2.x tool (catches accidental rewrites)', () => { + // Pin the first 60 chars of each description. Full description allowed to + // grow with new tip lines, but the leading sentence must not change — that + // is what AI agents key on for tool selection. + const snapshot = V1_2_X_TOOL_NAMES.map((name) => { + const t = toolByName(name); + return { + name: t.name, + description_head: t.description.slice(0, 60), + }; + }); + expect(snapshot).toMatchSnapshot(); + }); +}); diff --git a/tests/wait-batch.test.ts b/tests/wait-batch.test.ts new file mode 100644 index 0000000..cefa279 --- /dev/null +++ b/tests/wait-batch.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleWaitForSmsBatch, WaitForSmsBatchInput } from '../src/tools/v1_3/wait-batch.js'; +import type { VirtualSMSClient, Order } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +function order(o: Partial): Order { + return { + order_id: o.order_id ?? 'o', + phone_number: o.phone_number ?? '+1...', + status: o.status ?? 'waiting', + ...o, + } as Order; +} + +describe('virtualsms_wait_for_sms_batch', () => { + test('input validation — array bounds 1..20', () => { + expect(() => WaitForSmsBatchInput.parse({ order_ids: [] })).toThrow(); + const tooMany = Array.from({ length: 21 }, (_, i) => `o${i}`); + expect(() => WaitForSmsBatchInput.parse({ order_ids: tooMany })).toThrow(); + const ok = WaitForSmsBatchInput.parse({ order_ids: ['o1'] }); + expect(ok.timeout_seconds).toBe(120); + expect(ok.return_partial).toBe(true); + }); + + test('happy path — all SMS arrive on first poll', async () => { + const client = makeClient({ + getApiKey: () => undefined, // forces polling fallback (no WS) + getBaseUrl: () => 'https://example.com', + getOrder: vi.fn(async (id: string) => + order({ + order_id: id, + status: 'sms_received', + messages: [{ content: `Code is ${id.slice(-1)}1234`, sender: 'svc' }], + }) + ), + }); + const out = await handleWaitForSmsBatch( + client, + WaitForSmsBatchInput.parse({ order_ids: ['o1', 'o2', 'o3'], timeout_seconds: 5 }) + ); + const data = parseResult(out); + const received = data.received as Array<{ order_id: string; code: string }>; + expect(received.length).toBe(3); + expect(received.map((r) => r.order_id).sort()).toEqual(['o1', 'o2', 'o3']); + expect(data.timed_out).toEqual([]); + expect(data.errors).toEqual([]); + }); + + test('partial — some orders time out, return_partial=true returns what arrived', async () => { + const callCounts: Record = { o1: 0, o2: 0 }; + const client = makeClient({ + getApiKey: () => undefined, + getBaseUrl: () => 'https://example.com', + getOrder: vi.fn(async (id: string) => { + callCounts[id] = (callCounts[id] ?? 0) + 1; + if (id === 'o1') { + return order({ order_id: id, status: 'sms_received', messages: [{ content: 'code 5555' }] }); + } + // o2 stays waiting forever → times out + return order({ order_id: id, status: 'waiting' }); + }), + }); + const out = await handleWaitForSmsBatch( + client, + WaitForSmsBatchInput.parse({ order_ids: ['o1', 'o2'], timeout_seconds: 5 }) + ); + const data = parseResult(out); + const received = data.received as Array<{ order_id: string }>; + expect(received.map((r) => r.order_id)).toEqual(['o1']); + expect(data.timed_out).toEqual(['o2']); + expect(data.errors).toEqual([]); + }, 15_000); + + test('error path — getOrder rejects → recorded in errors[]', async () => { + const client = makeClient({ + getApiKey: () => undefined, + getBaseUrl: () => 'https://example.com', + getOrder: vi.fn(async (id: string) => { + if (id === 'o1') return order({ order_id: id, status: 'sms_received', messages: [{ content: 'code 1234' }] }); + throw new Error('Not found: o2'); + }), + }); + const out = await handleWaitForSmsBatch( + client, + WaitForSmsBatchInput.parse({ order_ids: ['o1', 'o2'], timeout_seconds: 5 }) + ); + const data = parseResult(out); + const errors = data.errors as Array<{ order_id: string; error: string }>; + expect(errors.length).toBe(1); + expect(errors[0].order_id).toBe('o2'); + expect(errors[0].error).toMatch(/Not found/); + const received = data.received as Array<{ order_id: string }>; + expect(received.map((r) => r.order_id)).toEqual(['o1']); + }); +}); diff --git a/tests/x402-info.test.ts b/tests/x402-info.test.ts new file mode 100644 index 0000000..0eef9b5 --- /dev/null +++ b/tests/x402-info.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test, vi } from 'vitest'; +import { handleX402Info, X402InfoInput } from '../src/tools/v1_3/x402-info.js'; +import type { VirtualSMSClient, X402Info } from '../src/client.js'; + +function makeClient(impl: Partial): VirtualSMSClient { + return impl as unknown as VirtualSMSClient; +} + +function parseResult(out: { content: Array<{ type: 'text'; text: string }> }): Record { + return JSON.parse(out.content[0].text); +} + +describe('virtualsms_x402_info', () => { + test('happy path — maps backend response to agent-friendly shape', async () => { + const fakeInfo: X402Info = { + enabled: true, + x402_version: 1, + networks: [ + { network: 'solana', token: 'USDC' }, + { network: 'solana', token: 'USDT' }, + { network: 'base', token: 'USDC' }, + ], + evm_relayer: '0xfEc54264350d97d9b63f9Cc415BAF708C4695F32', + solana_relayer: '7AJwx3J2qXnURXZmU5AotDeMUY5dDBqBFbweHLZ2UeUs', + min_topup_usd: 2, + max_topup_usd: 10000, + default_topup_usd: 5, + topup_endpoint: 'https://virtualsms.io/api/v1/x402/topup', + }; + const client = makeClient({ + getX402Info: vi.fn(async () => fakeInfo), + }); + const out = await handleX402Info(client, X402InfoInput.parse({})); + const data = parseResult(out); + expect(data.enabled).toBe(true); + const accepts = data.accepts as Array<{ network: string; asset: string }>; + expect(accepts.length).toBe(3); + expect(accepts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ network: 'solana', asset: 'USDC' }), + expect.objectContaining({ network: 'base', asset: 'USDC' }), + ]) + ); + expect(data.min_topup_usd).toBe(2); + expect(data.max_topup_usd).toBe(10000); + expect(typeof data.tip).toBe('string'); + }); + + test('NEVER exposes BSC/BNB in accepts even if backend leaks it', async () => { + const leaky: X402Info = { + enabled: true, + networks: [ + { network: 'base', token: 'USDC' }, + { network: 'bsc', token: 'USDC' }, // backend should never return this — but defend anyway + { network: 'binance', token: 'BNB' }, // alt name + { network: 'bnb', token: 'USDT' }, // another alt + ], + }; + const client = makeClient({ getX402Info: vi.fn(async () => leaky) }); + const out = await handleX402Info(client, X402InfoInput.parse({})); + const data = parseResult(out); + const accepts = data.accepts as Array<{ network: string }>; + const networks = accepts.map((a) => a.network.toLowerCase()); + expect(networks).not.toContain('bsc'); + expect(networks).not.toContain('binance'); + expect(networks).not.toContain('bnb'); + expect(networks).toContain('base'); + }); + + test('disabled — backend returns enabled:false → returns enabled:false (no payment_required)', async () => { + const client = makeClient({ + getX402Info: vi.fn(async () => ({ enabled: false, networks: [] }) as X402Info), + }); + const out = await handleX402Info(client, X402InfoInput.parse({})); + const data = parseResult(out); + expect(data.enabled).toBe(false); + expect(data.accepts).toEqual([]); + }); + + test('error path — backend 503 → graceful unsupported_on_this_backend', async () => { + const client = makeClient({ + getX402Info: vi.fn(async () => { + throw new Error('VirtualSMS server error (503).'); + }), + }); + const out = await handleX402Info(client, X402InfoInput.parse({})); + const data = parseResult(out); + expect(data.error).toBe('unsupported_on_this_backend'); + expect(data.enabled).toBe(false); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index aeaeecc..f90f6fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "tests", "vitest.config.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1c1ef78 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + environment: 'node', + globals: false, + testTimeout: 10000, + }, +});