diff --git a/.gitignore b/.gitignore index 3ff6a9f3..d4510e3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ node_modules/ dist/ +coverage/ +.env.test +.env.test.local *.js *.d.ts *.js.map !src/** +!test/** +!vitest.config.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a4276cef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## 0.1.0 — 2026-04-20 + +Initial release. + +### Tools (11) + +Read-only (enabled by default): +- `leadbay_login` — authenticate with email + password +- `leadbay_list_lenses` — list saved search configs +- `leadbay_discover_leads` — AI-recommended leads +- `leadbay_get_lead_profile` — full lead profile with AI scores and web insights +- `leadbay_get_lead_activities` — lead activity feed +- `leadbay_get_taste_profile` — organization ICP + intent tags + qualification questions +- `leadbay_get_contacts` — contacts for a lead +- `leadbay_get_quota` — enrichment credit balance + +Write (opt-in, `optional: true`): +- `leadbay_qualify_lead` — trigger AI qualification +- `leadbay_enrich_contacts` — enrich email/phone +- `leadbay_add_note` — add a note to a lead + +### Tests + +- Contract test: manifest ↔ code parity +- Unit tests: client error mapping, caching, tool branches +- Live smoke tests (opt-in via `LEADBAY_TEST_TOKEN`) diff --git a/README.md b/README.md index 94f05593..c4a80d98 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Install ```bash -openclaw plugins install leadclaw +openclaw plugins install @leadbay/openclaw-leadclaw ``` ## Setup @@ -43,6 +43,8 @@ openclaw plugins install leadclaw | `leadbay_list_lenses` | List available lenses (saved search configs) | | `leadbay_discover_leads` | Get AI-recommended leads from your active lens | | `leadbay_get_lead_profile` | Full lead profile with AI scores, qualification Q&A, and contacts | +| `leadbay_get_lead_activities` | Activity feed for a lead (notes, enrichments, status changes) | +| `leadbay_get_taste_profile` | Your ideal buyer profile, purchase-intent tags, and AI qualification questions | | `leadbay_get_contacts` | Get contacts for a lead (with enriched emails/phones if available) | | `leadbay_get_quota` | Check your enrichment credit balance | @@ -88,3 +90,52 @@ leadbay_discover_leads → leadbay_qualify_lead (for unscored leads) → (wait ~ - Node.js 22+ - A [Leadbay account](https://wow.leadbay.ai/?register=true) + +## Development + +```bash +npm install # installs deps + vitest +npm test # runs contract + unit + sanity tests (no network, no secrets) +npm run test:coverage # coverage report via v8 +npm run build # emits dist/ +``` + +### Test tiers + +- **Contract tests** (`test/contract.test.ts`) — assert that registered tools match `openclaw.plugin.json` exactly, schemas are valid, write tools are marked `optional: true`. This catches manifest drift at CI. +- **Unit tests** (`test/unit/**`) — error-code mapping, caching, tool branches. Use `mockHttp` from `test/harness.ts` to stub `node:https`. No network required. +- **Live smoke tests** (`test/smoke/**`) — opt-in. Set `LEADBAY_TEST_TOKEN` (and optionally `LEADBAY_TEST_BASE_URL`) and run: + ```bash + LEADBAY_TEST_TOKEN=u.xxx npm run test:smoke + ``` + Without the env var, these tests cleanly skip. Use a **dedicated test tenant** with a **read-only token** — smoke only hits read endpoints (`/users/me`, `/lenses`, taste profile). + +### CI recommendation + +- Run `npm test` on every PR — no secrets needed. +- Run `npm run test:smoke` on main merges or nightly, with the `LEADBAY_TEST_TOKEN` secret. + +## Publishing + +Publication-ready checks: + +```bash +npm run build # emits dist/ +npm test # contract + unit must be green +npm publish --access public --dry-run # validate npm package +``` + +### ClawHub (primary) + +```bash +clawhub package publish leadbay/leadclaw --dry-run +clawhub package publish leadbay/leadclaw +``` + +### npm (fallback) + +```bash +npm publish --access public +``` + +The `prepublishOnly` script wires both `build` and `test` into every publish, so a broken diff never ships. diff --git a/openclaw.plugin.json b/openclaw.plugin.json index c5fbfac9..f8c6f7aa 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,12 +2,15 @@ "id": "leadclaw", "name": "LeadClaw", "description": "Leadbay lead discovery, qualification, and contact enrichment", + "version": "0.1.0", "contracts": { "tools": [ "leadbay_login", "leadbay_list_lenses", "leadbay_discover_leads", "leadbay_get_lead_profile", + "leadbay_get_lead_activities", + "leadbay_get_taste_profile", "leadbay_get_contacts", "leadbay_get_quota", "leadbay_qualify_lead", @@ -25,6 +28,11 @@ "label": "API Base URL", "help": "Override API URL (for staging/dev)", "advanced": true + }, + "token": { + "label": "API Token", + "help": "Pre-set bearer token to skip the login step", + "sensitive": true } }, "configSchema": { diff --git a/package-lock.json b/package-lock.json index 4e59b0e7..848ead70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,920 @@ { - "name": "leadclaw", + "name": "@leadbay/openclaw-leadclaw", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "leadclaw", + "name": "@leadbay/openclaw-leadclaw", "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/node": "^25.6.0", - "typescript": "^5.5" + "@vitest/coverage-v8": "^2.1.0", + "typescript": "^5.5", + "vitest": "^2.1.0" }, "engines": { "node": ">=22" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "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": ">=12" + } + }, + "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": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "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": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "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": ">=12" + } + }, + "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": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "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": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "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": ">=12" + } + }, + "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": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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", + "optional": true, + "os": [ + "android" + ] + }, + "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", + "optional": true, + "os": [ + "android" + ] + }, + "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", + "optional": true, + "os": [ + "darwin" + ] + }, + "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", + "optional": true, + "os": [ + "darwin" + ] + }, + "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", + "optional": true, + "os": [ + "freebsd" + ] + }, + "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", + "optional": true, + "os": [ + "freebsd" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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", + "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": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", @@ -23,7 +922,1075 @@ "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": "~7.19.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "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/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/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": ">=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/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/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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=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==", + "dev": true, + "license": "MIT" + }, + "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": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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", + "engines": { + "node": ">= 14.16" + } + }, + "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/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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": ">=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/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/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/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "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/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/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": "^18.0.0 || >=20.0.0" + } + }, + "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": ">=14.0.0" + } + }, + "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": ">=14.0.0" } }, "node_modules/typescript": { @@ -46,6 +2013,286 @@ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" + }, + "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": "^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" + }, + "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/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": { + "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": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index b04f3cbd..ffe330f4 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,47 @@ { - "name": "leadclaw", + "name": "@leadbay/openclaw-leadclaw", "version": "0.1.0", "description": "OpenClaw plugin for Leadbay — AI lead discovery, qualification, and enrichment", "type": "module", "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/", + "openclaw.plugin.json", + "README.md", + "LICENSE", + "logo.png" + ], "openclaw": { - "type": "plugin", "extensions": [ "./dist/index.js" - ] + ], + "install": { + "npmSpec": "@leadbay/openclaw-leadclaw" + }, + "compat": { + "pluginApi": ">=2026.3.24-beta.2", + "minGatewayVersion": "2026.3.24-beta.2" + }, + "build": { + "openclawVersion": "2026.3.24-beta.2", + "pluginSdkVersion": "2026.3.24-beta.2" + } }, "scripts": { "build": "tsc", - "dev": "tsc --watch" + "dev": "tsc --watch", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:smoke": "vitest run --config vitest.smoke.config.ts", + "posttest": "echo 'Tip: run npm run test:smoke with LEADBAY_TEST_TOKEN to test the live API.'", + "prepublishOnly": "npm run build && npm test" }, "devDependencies": { "@types/node": "^25.6.0", - "typescript": "^5.5" + "@vitest/coverage-v8": "^2.1.0", + "typescript": "^5.5", + "vitest": "^2.1.0" }, "engines": { "node": ">=22" diff --git a/src/client.ts b/src/client.ts index 9cf87964..a91c632c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -137,69 +137,7 @@ export class LeadbayClient { } if (res.status < 200 || res.status >= 300) { - let parsed: any; - try { - parsed = JSON.parse(res.body); - } catch { - parsed = null; - } - - if (res.status === 401) { - throw this.makeError( - "AUTH_EXPIRED", - "Authentication token expired or invalid", - "Call leadbay_login to re-authenticate" - ); - } - - if (res.status === 402 || parsed?.error === "quota_exceeded") { - throw this.makeError( - "QUOTA_EXCEEDED", - "No enrichment credits remaining", - "Purchase more credits at app.leadbay.ai" - ); - } - - if (res.status === 403) { - const msg = parsed?.message || parsed?.error || ""; - if ( - typeof msg === "string" && - (msg.includes("suspend") || msg.includes("billing")) - ) { - throw this.makeError( - "BILLING_SUSPENDED", - "Account billing is suspended", - "Check billing at app.leadbay.ai" - ); - } - throw this.makeError( - "FORBIDDEN", - "Insufficient permissions", - "Check your account permissions" - ); - } - - if (res.status === 404) { - throw this.makeError( - "NOT_FOUND", - parsed?.message || "Resource not found", - "Verify the ID is correct" - ); - } - - if (res.status === 429) { - throw this.makeError( - "RATE_LIMITED", - "Too many requests", - "Wait a moment and try again" - ); - } - - throw this.makeError( - "API_ERROR", - parsed?.message || `API error (${res.status})`, - "Try again or check the Leadbay API status" - ); + throw this.mapErrorResponse(res.status, res.body); } return JSON.parse(res.body) as T; @@ -234,32 +172,74 @@ export class LeadbayClient { ); if (res.status < 200 || res.status >= 300) { - let parsed: any; - try { - parsed = JSON.parse(res.body); - } catch { - parsed = null; - } - - if (res.status === 401) { - throw this.makeError( - "AUTH_EXPIRED", - "Authentication token expired or invalid", - "Call leadbay_login to re-authenticate" - ); - } - - throw this.makeError( - "API_ERROR", - parsed?.message || `API error (${res.status})`, - "Try again or check the Leadbay API status" - ); + throw this.mapErrorResponse(res.status, res.body); } } finally { this.releaseSemaphore(); } } + private mapErrorResponse(status: number, rawBody: string): LeadbayError { + let parsed: any; + try { + parsed = JSON.parse(rawBody); + } catch { + parsed = null; + } + + if (status === 401) { + return this.makeError( + "AUTH_EXPIRED", + "Authentication token expired or invalid", + "Call leadbay_login to re-authenticate" + ); + } + if (status === 402 || parsed?.error === "quota_exceeded") { + return this.makeError( + "QUOTA_EXCEEDED", + "No enrichment credits remaining", + "Purchase more credits at app.leadbay.ai" + ); + } + if (status === 403) { + const msg = parsed?.message || parsed?.error || ""; + if ( + typeof msg === "string" && + (msg.includes("suspend") || msg.includes("billing")) + ) { + return this.makeError( + "BILLING_SUSPENDED", + "Account billing is suspended", + "Check billing at app.leadbay.ai" + ); + } + return this.makeError( + "FORBIDDEN", + "Insufficient permissions", + "Check your account permissions" + ); + } + if (status === 404) { + return this.makeError( + "NOT_FOUND", + parsed?.message || "Resource not found", + "Verify the ID is correct" + ); + } + if (status === 429) { + return this.makeError( + "RATE_LIMITED", + "Too many requests", + "Wait a moment and try again" + ); + } + return this.makeError( + "API_ERROR", + parsed?.message || `API error (${status})`, + "Try again or check the Leadbay API status" + ); + } + async resolveDefaultLens(): Promise { const now = Date.now(); if ( diff --git a/test/contract.test.ts b/test/contract.test.ts new file mode 100644 index 00000000..e08b572b --- /dev/null +++ b/test/contract.test.ts @@ -0,0 +1,128 @@ +/** + * Contract tests — the single most valuable tests in the suite. + * + * Enforces manifest ↔ code parity. When a tool is added in src/ or removed + * from openclaw.plugin.json, these fail with a named-diff error. No magic + * tool counts. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { createTestApi } from "./harness.js"; +import { register } from "../src/index.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const manifestPath = path.resolve(__dirname, "..", "openclaw.plugin.json"); +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + +const WRITE_TOOLS = new Set([ + "leadbay_qualify_lead", + "leadbay_enrich_contacts", + "leadbay_add_note", +]); + +describe("contract: manifest ↔ code parity", () => { + it("registered tools match openclaw.plugin.json contracts.tools exactly", () => { + const t = createTestApi({ region: "us" }); + register(t.api as any); + + const registered = new Set(t.tools.keys()); + const declared = new Set(manifest.contracts.tools); + + const added = [...registered].filter((n) => !declared.has(n)); + const missing = [...declared].filter((n) => !registered.has(n)); + + if (added.length || missing.length) { + throw new Error( + `Manifest drift.\n` + + ` registered in code but MISSING from openclaw.plugin.json: [${added.join( + ", " + )}]\n` + + ` declared in openclaw.plugin.json but NOT registered: [${missing.join(", ")}]\n` + + `Fix: either update openclaw.plugin.json contracts.tools or delete the unregistered tool.` + ); + } + + expect([...registered].sort()).toEqual([...declared].sort()); + }); + + it("every registered tool has a valid JSON-schema parameters object", () => { + const t = createTestApi({ region: "us" }); + register(t.api as any); + + for (const [name, tool] of t.tools) { + expect(tool.parameters, `${name} parameters`).toBeTypeOf("object"); + const p = tool.parameters as any; + expect(p.type, `${name}.parameters.type`).toBe("object"); + expect(p.properties, `${name}.parameters.properties`).toBeTypeOf("object"); + } + }); + + it("write tools are marked optional; read tools are not", () => { + const t = createTestApi({ region: "us" }); + register(t.api as any); + + for (const [name, tool] of t.tools) { + if (WRITE_TOOLS.has(name)) { + expect(tool.optional, `write tool ${name} must be optional:true`).toBe(true); + } else { + expect(tool.optional, `read tool ${name} must NOT be optional`).not.toBe(true); + } + } + }); + + it("every registered tool has a non-empty description", () => { + const t = createTestApi({ region: "us" }); + register(t.api as any); + + for (const [name, tool] of t.tools) { + expect(tool.description, `${name}.description`).toBeTypeOf("string"); + expect((tool.description as string).length, `${name}.description length`).toBeGreaterThan(10); + } + }); +}); + +describe("contract: register() behaviour", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("emits logger.warn and registers no tools when region+baseUrl missing", () => { + const t = createTestApi({ region: "xx" }); // invalid region → no baseUrl lookup + register(t.api as any); + expect(t.tools.size).toBe(0); + expect(t.logs.some((l) => l.level === "warn" && /region/i.test(l.msg))).toBe(true); + }); + + it("registers 11 tools when region=us (valid default)", () => { + const t = createTestApi({ region: "us" }); + register(t.api as any); + expect(t.tools.size).toBe(11); + }); + + it("calls client.setToken and logs info when cfg.token is provided", async () => { + const clientModule = await import("../src/client.js"); + const spy = vi.spyOn(clientModule.LeadbayClient.prototype, "setToken"); + + const t = createTestApi({ region: "us", token: "u.preconfig-token" }); + register(t.api as any); + + expect(spy).toHaveBeenCalledWith("u.preconfig-token"); + expect(t.logs.some((l) => l.level === "info" && /preconfigured/i.test(l.msg))).toBe( + true + ); + }); + + it("manifest contracts.tools matches the openclaw.plugin.json schema", () => { + // Sanity: manifest is a valid JSON schema shape + expect(manifest.id).toBe("leadclaw"); + expect(manifest.configSchema).toBeTypeOf("object"); + expect(Array.isArray(manifest.contracts.tools)).toBe(true); + }); +}); diff --git a/test/harness.ts b/test/harness.ts new file mode 100644 index 00000000..72179d33 --- /dev/null +++ b/test/harness.ts @@ -0,0 +1,221 @@ +/** + * Test harness for the LeadClaw OpenClaw tool plugin. + * + * Public API (stable): + * - createTestApi(config?) fake `api` object capturing registerTool calls + * - executeTool(testApi, ...) invoke a registered tool's execute() + * - mockHttp(scripts) predicate-match mock for node:https + * - resetHttpMock() clear scripts between tests + * + * Do NOT import from this file's internals in test files. The public API above + * is all you need. When the OpenClaw SDK ships a tool-plugin test helper + * (openclaw/plugin-sdk/testing), this file is the one-file swap. + * + * mockHttp note: matches each script once by {method, path} against the + * outgoing https.request. Concurrent requests (e.g. Promise.allSettled in + * get-lead-profile) are handled because matching is by-request, not FIFO. + */ + +import { vi, expect } from "vitest"; +import { EventEmitter } from "node:events"; + +export interface RegisteredTool { + name: string; + description: string; + parameters: unknown; + optional?: boolean; + execute: (id: string, params: unknown) => unknown | Promise; +} + +export interface TestApi { + api: { + pluginConfig: Record; + logger: { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; + }; + registerTool: (tool: RegisteredTool) => void; + }; + tools: Map; + logs: { level: "info" | "warn" | "error"; msg: string }[]; +} + +export function createTestApi(pluginConfig: Record = {}): TestApi { + const tools = new Map(); + const logs: { level: "info" | "warn" | "error"; msg: string }[] = []; + const api = { + pluginConfig, + logger: { + info: (msg: string) => logs.push({ level: "info", msg }), + warn: (msg: string) => logs.push({ level: "warn", msg }), + error: (msg: string) => logs.push({ level: "error", msg }), + }, + registerTool: (tool: RegisteredTool) => { + tools.set(tool.name, tool); + }, + }; + return { api, tools, logs }; +} + +export async function executeTool( + testApi: TestApi, + name: string, + params: unknown = {} +): Promise { + const tool = testApi.tools.get(name); + if (!tool) { + throw new Error( + `executeTool: tool "${name}" not registered. Registered: [${Array.from( + testApi.tools.keys() + ).join(", ")}]` + ); + } + return tool.execute("test-id", params); +} + +// ---------------------------------------------------------------------------- +// mockHttp — predicate-match mock for node:https default import. +// ---------------------------------------------------------------------------- + +export interface RequestScript { + method: string; + path: string | RegExp; + status: number; + body?: string | object; + error?: Error; +} + +interface CapturedRequest { + method: string; + url: string; + path: string; + body?: string; + headers: Record; +} + +// Hoisted so vi.mock's factory can see it. Not exported — access via helpers. +const mockHttpState = vi.hoisted(() => ({ + scripts: [] as Array<{ script: any; consumed: boolean }>, + requests: [] as any[], +})); + +export function mockHttp(scripts: RequestScript[]): { requests: CapturedRequest[] } { + mockHttpState.scripts = scripts.map((s) => ({ script: s, consumed: false })); + mockHttpState.requests = []; + return { requests: mockHttpState.requests as CapturedRequest[] }; +} + +export function resetHttpMock(): void { + mockHttpState.scripts = []; + mockHttpState.requests = []; +} + +export function getHttpRequests(): CapturedRequest[] { + return mockHttpState.requests as CapturedRequest[]; +} + +function pathMatches(pattern: string | RegExp, path: string): boolean { + if (pattern instanceof RegExp) return pattern.test(path); + return pattern === path; +} + +// The actual https.request replacement. Must match node:https contract closely +// enough that src/client.ts and src/tools/login.ts don't know the difference. +function fakeHttpsRequest( + options: any, + callback?: (res: any) => void +): any { + const method = options.method ?? "GET"; + const path = options.path ?? "/"; + const hostname = options.hostname ?? "localhost"; + const url = `https://${hostname}${path}`; + + const captured: CapturedRequest = { + method, + url, + path, + headers: options.headers ?? {}, + }; + mockHttpState.requests.push(captured); + + const req = new EventEmitter() as any; + let bodyBuffer = ""; + req.write = (chunk: string | Buffer) => { + bodyBuffer += chunk.toString(); + }; + req.end = () => { + captured.body = bodyBuffer || undefined; + + const entry = mockHttpState.scripts.find( + (s) => !s.consumed && s.script.method === method && pathMatches(s.script.path, path) + ); + + if (!entry) { + const registered = mockHttpState.scripts.map( + (s: any) => `${s.script.method} ${s.script.path}${s.consumed ? " [used]" : ""}` + ); + const err = new Error( + `mockHttp: no script matched ${method} ${path}\n registered: [${registered.join( + ", " + )}]` + ); + setImmediate(() => req.emit("error", err)); + return; + } + + entry.consumed = true; + + if (entry.script.error) { + setImmediate(() => req.emit("error", entry.script.error)); + return; + } + + const res = new EventEmitter() as any; + res.statusCode = entry.script.status; + setImmediate(() => { + if (callback) callback(res); + const bodyStr = + typeof entry.script.body === "string" + ? entry.script.body + : entry.script.body != null + ? JSON.stringify(entry.script.body) + : ""; + if (bodyStr) res.emit("data", Buffer.from(bodyStr, "utf8")); + res.emit("end"); + }); + }; + return req; +} + +// vi.mock calls must be hoisted. We install the mock at the top of every test +// file that needs it via `vi.mock("node:https", () => ...)`. The shared factory +// below is what those mocks reference. +export const httpsMockModule = { + default: { request: fakeHttpsRequest }, + request: fakeHttpsRequest, +}; + +// Convenience for test files. Use at module scope ABOVE any src imports: +// vi.mock("node:https", () => httpsMockFactory()); +export function httpsMockFactory() { + return { + default: { request: fakeHttpsRequest }, + request: fakeHttpsRequest, + }; +} + +// Assertion helper: check that all scripts were consumed (no leftover mocks). +export function expectAllScriptsConsumed(): void { + const leftover = mockHttpState.scripts + .filter((s) => !s.consumed) + .map((s: any) => `${s.script.method} ${s.script.path}`); + if (leftover.length) { + throw new Error( + `mockHttp: ${leftover.length} unused script(s): [${leftover.join(", ")}]` + ); + } +} + +// Keep `expect` imported so vitest.config types resolve consistently. +void expect; diff --git a/test/sanity.test.ts b/test/sanity.test.ts new file mode 100644 index 00000000..469a1972 --- /dev/null +++ b/test/sanity.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from "vitest"; + +describe("sanity", () => { + it("math still works", () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/test/smoke/leadbay.live.test.ts b/test/smoke/leadbay.live.test.ts new file mode 100644 index 00000000..176f2e64 --- /dev/null +++ b/test/smoke/leadbay.live.test.ts @@ -0,0 +1,63 @@ +/** + * LIVE smoke tests against the real Leadbay API. + * + * Opt-in — set LEADBAY_TEST_TOKEN (and optionally LEADBAY_TEST_BASE_URL) to run. + * Excluded from the default `npm test` via vitest.config.ts; run explicitly: + * + * npm run test:smoke + * + * Governance (important before enabling in CI): + * - Use a DEDICATED test tenant — not a production Leadbay org + * - Use a LEAST-PRIVILEGED, READ-ONLY token + * - Smoke only hits read endpoints (/lenses, /users/me, taste profile) + * - No live login (email/password) — use token preconfigure to avoid + * session churn and audit-trail noise + * - No enrich / qualify / add-note calls from smoke — those cost credits + */ + +import { describe, it, expect } from "vitest"; +import { LeadbayClient } from "../../src/client.js"; + +const TOKEN = process.env.LEADBAY_TEST_TOKEN; +const BASE_URL = process.env.LEADBAY_TEST_BASE_URL ?? "https://api-us.leadbay.app"; + +const runLive = !!TOKEN; +if (!runLive) { + // eslint-disable-next-line no-console + console.log( + "[smoke] SMOKE_SKIPPED: set LEADBAY_TEST_TOKEN to run live smoke tests" + ); +} + +describe.skipIf(!runLive)("LeadClaw live smoke (read-only endpoints)", () => { + const client = new LeadbayClient(BASE_URL, TOKEN); + + it("/users/me returns an organization with numeric ai_credits", async () => { + const me = await client.request("GET", "/users/me"); + expect(me.organization).toBeTypeOf("object"); + expect(me.organization.id).toBeTypeOf("string"); + expect(typeof me.organization.billing?.ai_credits).toBe("number"); + }); + + it("/lenses returns a non-empty array with expected shape", async () => { + const lenses = await client.request("GET", "/lenses"); + expect(Array.isArray(lenses)).toBe(true); + expect(lenses.length).toBeGreaterThan(0); + const l = lenses[0]; + expect(l.id).toBeTypeOf("number"); + expect(l.name).toBeTypeOf("string"); + expect(typeof l.is_last_active === "boolean").toBe(true); + }); + + it("resolveDefaultLens returns a numeric lens id", async () => { + const id = await client.resolveDefaultLens(); + expect(typeof id).toBe("number"); + }); + + it("resolveTasteProfile returns the three-part shape (even if partial)", async () => { + const tp = await client.resolveTasteProfile(); + expect(tp).toHaveProperty("idealBuyerProfile"); + expect(Array.isArray(tp.purchaseIntentTags)).toBe(true); + expect(Array.isArray(tp.qualificationQuestions)).toBe(true); + }); +}); diff --git a/test/unit/client.test.ts b/test/unit/client.test.ts new file mode 100644 index 00000000..bfd6f0b5 --- /dev/null +++ b/test/unit/client.test.ts @@ -0,0 +1,339 @@ +/** + * Unit tests for LeadbayClient — the error-mapping table is the primary value. + * Uses mockHttp() from harness.ts (predicate match, opinionated errors on + * unmatched requests). + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + createTestApi, + mockHttp, + resetHttpMock, + httpsMockFactory, +} from "../harness.js"; + +// Install the node:https mock at module scope BEFORE any src imports. +vi.mock("node:https", () => httpsMockFactory()); + +import { LeadbayClient } from "../../src/client.js"; + +const BASE = "https://api-us.leadbay.app"; + +beforeEach(() => { + resetHttpMock(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("LeadbayClient.request — HTTP status → error code mapping", () => { + const cases: Array<[number, string, object | string | undefined, string, string?]> = [ + [401, "AUTH_EXPIRED", { message: "expired" }, "re-authenticate"], + [402, "QUOTA_EXCEEDED", { message: "no credits" }, "Purchase more credits"], + [403, "BILLING_SUSPENDED", { message: "account suspended" }, "billing"], + [403, "BILLING_SUSPENDED", { error: "billing_locked" }, "billing"], + [403, "FORBIDDEN", { message: "forbidden" }, "permissions"], + [404, "NOT_FOUND", { message: "nope" }, "ID is correct"], + [429, "RATE_LIMITED", {}, "Wait"], + [500, "API_ERROR", { message: "boom" }, "Try again"], + [500, "API_ERROR", "not-json-body", "Try again", "API error (500)"], + [418, "API_ERROR", {}, "Try again"], + ]; + + it.each(cases)( + "HTTP %i → error code %s", + async (status, expectedCode, body, _expectedHint, expectedMessage) => { + mockHttp([{ method: "GET", path: "/1.5/lenses", status, body }]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.request("GET", "/lenses")).rejects.toMatchObject({ + error: true, + code: expectedCode, + }); + } + ); + + it("quota_exceeded body with non-402 status still maps to QUOTA_EXCEEDED", async () => { + mockHttp([ + { method: "GET", path: "/1.5/x", status: 400, body: { error: "quota_exceeded" } }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.request("GET", "/x")).rejects.toMatchObject({ + code: "QUOTA_EXCEEDED", + }); + }); + + it("status 204 → returns null (no JSON parse attempted)", async () => { + mockHttp([{ method: "POST", path: "/1.5/leads/x/web_fetch", status: 204 }]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.request("POST", "/leads/x/web_fetch")).resolves.toBeNull(); + }); + + it("2xx body is parsed as JSON", async () => { + mockHttp([ + { + method: "GET", + path: "/1.5/users/me", + status: 200, + body: { id: "user-1", email: "a@b.com" }, + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.request("GET", "/users/me")).resolves.toEqual({ + id: "user-1", + email: "a@b.com", + }); + }); + + it("throws NOT_AUTHENTICATED without a token — no network request", async () => { + mockHttp([]); + const client = new LeadbayClient(BASE); + await expect(client.request("GET", "/lenses")).rejects.toMatchObject({ + code: "NOT_AUTHENTICATED", + }); + }); + + it("requestVoid also enforces auth and 401 handling", async () => { + const client = new LeadbayClient(BASE); + await expect(client.requestVoid("POST", "/x")).rejects.toMatchObject({ + code: "NOT_AUTHENTICATED", + }); + + mockHttp([{ method: "POST", path: "/1.5/x", status: 401, body: {} }]); + const client2 = new LeadbayClient(BASE, "u.test-token"); + await expect(client2.requestVoid("POST", "/x")).rejects.toMatchObject({ + code: "AUTH_EXPIRED", + }); + }); + + it("Content-Type is set only when a body is provided", async () => { + const { requests } = mockHttp([ + { method: "GET", path: "/1.5/a", status: 200, body: {} }, + { method: "POST", path: "/1.5/b", status: 200, body: {} }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await client.request("GET", "/a"); + await client.request("POST", "/b", { x: 1 }); + expect(requests[0].headers["Content-Type"]).toBeUndefined(); + expect(requests[1].headers["Content-Type"]).toBe("application/json"); + }); +}); + +describe("LeadbayClient.resolveDefaultLens", () => { + it("picks is_last_active first", async () => { + mockHttp([ + { + method: "GET", + path: "/1.5/lenses", + status: 200, + body: [ + { id: 1, name: "A", is_last_active: false, is_default: true }, + { id: 2, name: "B", is_last_active: true, is_default: false }, + { id: 3, name: "C", is_last_active: false, is_default: false }, + ], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.resolveDefaultLens()).resolves.toBe(2); + }); + + it("falls back to is_default when nothing is last_active", async () => { + mockHttp([ + { + method: "GET", + path: "/1.5/lenses", + status: 200, + body: [ + { id: 1, name: "A", is_last_active: false, is_default: false }, + { id: 2, name: "B", is_last_active: false, is_default: true }, + ], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.resolveDefaultLens()).resolves.toBe(2); + }); + + it("falls back to first lens when no flags set", async () => { + mockHttp([ + { + method: "GET", + path: "/1.5/lenses", + status: 200, + body: [ + { id: 7, name: "A", is_last_active: false, is_default: false }, + { id: 8, name: "B", is_last_active: false, is_default: false }, + ], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.resolveDefaultLens()).resolves.toBe(7); + }); + + it("empty lens list throws NO_LENS", async () => { + mockHttp([{ method: "GET", path: "/1.5/lenses", status: 200, body: [] }]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.resolveDefaultLens()).rejects.toMatchObject({ + code: "NO_LENS", + }); + }); + + it("caches within 5-minute TTL — second call makes no new HTTP request", async () => { + const { requests } = mockHttp([ + { + method: "GET", + path: "/1.5/lenses", + status: 200, + body: [{ id: 42, name: "X", is_last_active: true, is_default: false }], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await client.resolveDefaultLens(); + await client.resolveDefaultLens(); + expect(requests.length).toBe(1); + }); + + it("re-fetches after 5-minute TTL expires", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-04-20T00:00:00Z")); + const { requests } = mockHttp([ + { + method: "GET", + path: "/1.5/lenses", + status: 200, + body: [{ id: 1, name: "X", is_last_active: true, is_default: false }], + }, + { + method: "GET", + path: "/1.5/lenses", + status: 200, + body: [{ id: 2, name: "Y", is_last_active: true, is_default: false }], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.resolveDefaultLens()).resolves.toBe(1); + vi.setSystemTime(new Date("2026-04-20T00:06:00Z")); // 6 min later + await expect(client.resolveDefaultLens()).resolves.toBe(2); + expect(requests.length).toBe(2); + }); +}); + +describe("LeadbayClient.resolveOrgId", () => { + it("caches permanently — second call makes no new HTTP request", async () => { + const { requests } = mockHttp([ + { + method: "GET", + path: "/1.5/users/me", + status: 200, + body: { id: "u", email: "a@b.com", organization: { id: "org-1" } }, + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await expect(client.resolveOrgId()).resolves.toBe("org-1"); + await expect(client.resolveOrgId()).resolves.toBe("org-1"); + expect(requests.length).toBe(1); + }); +}); + +describe("LeadbayClient.resolveTasteProfile — partial-result resilience", () => { + const meBody = { + id: "u", + email: "a@b.com", + organization: { id: "org-1" }, + }; + + it("returns full result when all three sub-requests succeed", async () => { + mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBody }, + { + method: "GET", + path: "/1.5/organizations/org-1/ideal_buyer_profile", + status: 200, + body: { summary: "ideal" }, + }, + { + method: "GET", + path: "/1.5/organizations/org-1/purchase_intent_tags", + status: 200, + body: [{ tag: "buy-now" }], + }, + { + method: "GET", + path: "/1.5/organizations/org-1/ai_agent_questions", + status: 200, + body: [{ question: "q1" }], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + const tp = await client.resolveTasteProfile(); + expect(tp.idealBuyerProfile).toEqual({ summary: "ideal" }); + expect(tp.purchaseIntentTags).toHaveLength(1); + expect(tp.qualificationQuestions).toHaveLength(1); + }); + + it("returns partial result when IBP rejects", async () => { + mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBody }, + { + method: "GET", + path: "/1.5/organizations/org-1/ideal_buyer_profile", + status: 404, + body: {}, + }, + { + method: "GET", + path: "/1.5/organizations/org-1/purchase_intent_tags", + status: 200, + body: [{ tag: "a" }], + }, + { + method: "GET", + path: "/1.5/organizations/org-1/ai_agent_questions", + status: 200, + body: [], + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + const tp = await client.resolveTasteProfile(); + expect(tp.idealBuyerProfile).toBeNull(); + expect(tp.purchaseIntentTags).toHaveLength(1); + expect(tp.qualificationQuestions).toHaveLength(0); + }); + + it("returns empty arrays when tags/questions reject but IBP succeeds", async () => { + mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBody }, + { + method: "GET", + path: "/1.5/organizations/org-1/ideal_buyer_profile", + status: 200, + body: { summary: "ok" }, + }, + { + method: "GET", + path: "/1.5/organizations/org-1/purchase_intent_tags", + status: 500, + body: {}, + }, + { + method: "GET", + path: "/1.5/organizations/org-1/ai_agent_questions", + status: 500, + body: {}, + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + const tp = await client.resolveTasteProfile(); + expect(tp.idealBuyerProfile).toEqual({ summary: "ok" }); + expect(tp.purchaseIntentTags).toEqual([]); + expect(tp.qualificationQuestions).toEqual([]); + }); +}); + +describe("LeadbayClient — auth state", () => { + it("isAuthenticated reflects token state", () => { + const c = new LeadbayClient(BASE); + expect(c.isAuthenticated).toBe(false); + c.setToken("u.new"); + expect(c.isAuthenticated).toBe(true); + }); +}); diff --git a/test/unit/tools/enrich-contacts.test.ts b/test/unit/tools/enrich-contacts.test.ts new file mode 100644 index 00000000..ea9d6fac --- /dev/null +++ b/test/unit/tools/enrich-contacts.test.ts @@ -0,0 +1,220 @@ +/** + * Tests for leadbay_enrich_contacts. + * Critical invariants: paid-path fallback to org, paid-success never triggers + * org (no double-charge), URL literal for query-param drift detection. + */ + +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + createTestApi, + executeTool, + mockHttp, + resetHttpMock, + httpsMockFactory, +} from "../../harness.js"; + +vi.mock("node:https", () => httpsMockFactory()); + +import { LeadbayClient } from "../../../src/client.js"; +import { registerEnrichContacts } from "../../../src/tools/enrich-contacts.js"; + +const BASE = "https://api-us.leadbay.app"; + +const meBodyFull = { + id: "u", + email: "a@b.com", + organization: { id: "org-1", billing: { ai_credits: 10 } }, +}; +const meBodyZeroCredits = { + id: "u", + email: "a@b.com", + organization: { id: "org-1", billing: { ai_credits: 0 } }, +}; + +beforeEach(() => { + resetHttpMock(); +}); + +function setup() { + const t = createTestApi({}); + const client = new LeadbayClient(BASE, "u.test-token"); + registerEnrichContacts(t.api as any, client); + return t; +} + +describe("leadbay_enrich_contacts — validation", () => { + it("both email=false and phone=false throws INVALID_PARAMS", async () => { + mockHttp([]); + const t = setup(); + await expect( + executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + email: false, + phone: false, + }) + ).rejects.toMatchObject({ code: "INVALID_PARAMS" }); + }); +}); + +describe("leadbay_enrich_contacts — quota advisory", () => { + it("quota 0 credits → throws QUOTA_EXCEEDED without enriching", async () => { + const { requests } = mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBodyZeroCredits }, + ]); + const t = setup(); + await expect( + executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + }) + ).rejects.toMatchObject({ code: "QUOTA_EXCEEDED" }); + // Only the /users/me call happened; no enrich call was made + expect(requests.filter((r) => r.path.includes("/enrich"))).toHaveLength(0); + }); + + it("advisory-check failure does NOT block enrichment", async () => { + mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 500, body: {} }, + { + method: "POST", + path: /\/1\.5\/leads\/L1\/enrich\/contacts\/C1\/enrich/, + status: 204, + }, + ]); + const t = setup(); + const result: any = await executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + }); + expect(result.triggered).toBe(true); + // credits_remaining is null because the advisory check failed + expect(result.credits_remaining).toBeNull(); + }); +}); + +describe("leadbay_enrich_contacts — paid → org fallback", () => { + it("paid path 404 → falls back to org-contacts path", async () => { + const { requests } = mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBodyFull }, + { + method: "POST", + path: /\/leads\/L1\/enrich\/contacts\/C1\/enrich/, + status: 404, + body: { message: "not found" }, + }, + { + method: "POST", + path: /\/leads\/L1\/contacts\/C1\/enrich/, + status: 204, + }, + ]); + const t = setup(); + const result: any = await executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + }); + expect(result.triggered).toBe(true); + + const paths = requests.map((r) => r.path); + expect(paths.some((p) => p.includes("/enrich/contacts/"))).toBe(true); + expect( + paths.some( + (p) => p.includes("/leads/L1/contacts/") && p.includes("/enrich") + ) + ).toBe(true); + }); + + it("paid path 500 error propagates — org path is NOT tried", async () => { + const { requests } = mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBodyFull }, + { + method: "POST", + path: /\/leads\/L1\/enrich\/contacts\/C1\/enrich/, + status: 500, + body: { message: "boom" }, + }, + ]); + const t = setup(); + await expect( + executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + }) + ).rejects.toMatchObject({ code: "API_ERROR" }); + + // Ensure the org-contacts fallback was NOT tried + const orgFallbackCalled = requests.some((r) => + /\/leads\/L1\/contacts\//.test(r.path) && /\/enrich$/.test(r.path) + ); + expect(orgFallbackCalled).toBe(false); + }); + + it("paid success → org endpoint is NOT called (no double-charge)", async () => { + const { requests } = mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBodyFull }, + { + method: "POST", + path: /\/leads\/L1\/enrich\/contacts\/C1\/enrich/, + status: 204, + }, + ]); + const t = setup(); + const result: any = await executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + }); + expect(result.triggered).toBe(true); + + const orgCalled = requests.some( + (r) => + /\/leads\/L1\/contacts\//.test(r.path) && /\/enrich/.test(r.path) + ); + expect(orgCalled).toBe(false); + }); +}); + +describe("leadbay_enrich_contacts — URL + response shape", () => { + it("emits exact URL with email/phone query params as literal strings", async () => { + const { requests } = mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBodyFull }, + { + method: "POST", + path: "/1.5/leads/L1/enrich/contacts/C1/enrich?email=true&phone=false", + status: 204, + }, + ]); + const t = setup(); + await executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + email: true, + phone: false, + }); + const paidCall = requests.find((r) => + r.path.includes("/enrich/contacts/C1/enrich") + ); + expect(paidCall!.path).toBe( + "/1.5/leads/L1/enrich/contacts/C1/enrich?email=true&phone=false" + ); + }); + + it("response includes credits_remaining from advisory check", async () => { + mockHttp([ + { method: "GET", path: "/1.5/users/me", status: 200, body: meBodyFull }, + { + method: "POST", + path: /\/enrich\/contacts\/.*\/enrich/, + status: 204, + }, + ]); + const t = setup(); + const result: any = await executeTool(t, "leadbay_enrich_contacts", { + leadId: "L1", + contactId: "C1", + }); + expect(result.credits_remaining).toBe(10); + expect(result.email_requested).toBe(true); + expect(result.phone_requested).toBe(true); + }); +}); diff --git a/test/unit/tools/get-lead-profile.test.ts b/test/unit/tools/get-lead-profile.test.ts new file mode 100644 index 00000000..9d11879a --- /dev/null +++ b/test/unit/tools/get-lead-profile.test.ts @@ -0,0 +1,269 @@ +/** + * Tests for leadbay_get_lead_profile. + * Critical invariants: lead-fetch failure is fatal; the other four sub-requests + * degrade to partial results; contact merge tags source correctly. + */ + +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + createTestApi, + executeTool, + mockHttp, + resetHttpMock, + httpsMockFactory, +} from "../../harness.js"; + +vi.mock("node:https", () => httpsMockFactory()); + +import { LeadbayClient } from "../../../src/client.js"; +import { registerGetLeadProfile } from "../../../src/tools/get-lead-profile.js"; + +const BASE = "https://api-us.leadbay.app"; + +const LENS = 7; +const LEAD = "lead-1"; + +const minimalLead = { + id: LEAD, + name: "Acme Corp", + score: 80, + ai_agent_lead_score: 75, + location: "SF", + description: "desc", + short_description: "short", + size: "50-200", + website: "acme.com", + logo: "logo.png", + ai_summary: null, + split_ai_summary: null, + tags: [], + phone_numbers: [], + keywords: [], + contacts_count: 3, + recommended_contact_title: null, + recommended_contact: null, + web_fetch_in_progress: false, +}; + +beforeEach(() => { + resetHttpMock(); +}); + +function setup() { + const t = createTestApi({}); + const client = new LeadbayClient(BASE, "u.test-token"); + registerGetLeadProfile(t.api as any, client); + return t; +} + +describe("leadbay_get_lead_profile — success path", () => { + it("returns all sections when every sub-request succeeds", async () => { + mockHttp([ + { method: "GET", path: `/1.5/lenses/${LENS}/leads/${LEAD}`, status: 200, body: minimalLead }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/ai_agent_responses`, + status: 200, + body: [ + { + question: "Is it a good fit?", + score: 90, + response: "yes", + computed_at: "2026-04-20", + outdated_at: null, + }, + ], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/contacts?IncludeEnriched=true`, + status: 200, + body: [ + { + id: "c-org-1", + first_name: "Jane", + last_name: "Doe", + email: "jane@acme.com", + job_title: "CTO", + recommended: true, + enrichment: null, + }, + ], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/enrich/contacts?IncludeEnriched=true`, + status: 200, + body: [ + { + id: "c-paid-1", + first_name: "Bob", + last_name: "Smith", + email: "bob@acme.com", + job_title: "VP Eng", + recommended: false, + enrichment: null, + }, + ], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/web_fetch`, + status: 200, + body: { + content: { company_profile: "profile text" }, + fetch_at: "2026-04-20T00:00:00Z", + }, + }, + ]); + const t = setup(); + const result: any = await executeTool(t, "leadbay_get_lead_profile", { + leadId: LEAD, + lensId: LENS, + }); + + expect(result.lead.name).toBe("Acme Corp"); + expect(result.qualification).toHaveLength(1); + expect(result.contacts).toHaveLength(2); + expect(result.web_insights).toEqual({ company_profile: "profile text" }); + }); +}); + +describe("leadbay_get_lead_profile — partial-result resilience", () => { + function baseScripts(leadStatus = 200, leadBody: any = minimalLead) { + return [ + { + method: "GET", + path: `/1.5/lenses/${LENS}/leads/${LEAD}`, + status: leadStatus, + body: leadBody, + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/ai_agent_responses`, + status: 200, + body: [], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/contacts?IncludeEnriched=true`, + status: 200, + body: [], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/enrich/contacts?IncludeEnriched=true`, + status: 200, + body: [], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/web_fetch`, + status: 200, + body: { content: null, fetch_at: null }, + }, + ]; + } + + it("lead fetch rejects (fatal) → throws", async () => { + mockHttp(baseScripts(404, { message: "no lead" }) as any); + const t = setup(); + await expect( + executeTool(t, "leadbay_get_lead_profile", { leadId: LEAD, lensId: LENS }) + ).rejects.toMatchObject({ code: "NOT_FOUND" }); + }); + + it("qualification rejects → qualification is null, other sections return", async () => { + const scripts = baseScripts(); + scripts[1] = { + method: "GET", + path: `/1.5/leads/${LEAD}/ai_agent_responses`, + status: 500, + body: {} as any, + }; + mockHttp(scripts as any); + const t = setup(); + const result: any = await executeTool(t, "leadbay_get_lead_profile", { + leadId: LEAD, + lensId: LENS, + }); + expect(result.qualification).toBeNull(); + expect(result.lead.id).toBe(LEAD); + }); + + it("both contacts endpoints reject → contacts is empty array (not null)", async () => { + const scripts = baseScripts(); + scripts[2] = { + method: "GET", + path: `/1.5/leads/${LEAD}/contacts?IncludeEnriched=true`, + status: 500, + body: {} as any, + }; + scripts[3] = { + method: "GET", + path: `/1.5/leads/${LEAD}/enrich/contacts?IncludeEnriched=true`, + status: 500, + body: {} as any, + }; + mockHttp(scripts as any); + const t = setup(); + const result: any = await executeTool(t, "leadbay_get_lead_profile", { + leadId: LEAD, + lensId: LENS, + }); + expect(result.contacts).toEqual([]); + }); + + it("web_fetch rejects → web_insights is null", async () => { + const scripts = baseScripts(); + scripts[4] = { + method: "GET", + path: `/1.5/leads/${LEAD}/web_fetch`, + status: 500, + body: {} as any, + }; + mockHttp(scripts as any); + const t = setup(); + const result: any = await executeTool(t, "leadbay_get_lead_profile", { + leadId: LEAD, + lensId: LENS, + }); + expect(result.web_insights).toBeNull(); + }); +}); + +describe("leadbay_get_lead_profile — contact source tagging", () => { + it("tags org contacts as 'org' and paid contacts as 'paid'", async () => { + mockHttp([ + { method: "GET", path: `/1.5/lenses/${LENS}/leads/${LEAD}`, status: 200, body: minimalLead }, + { method: "GET", path: `/1.5/leads/${LEAD}/ai_agent_responses`, status: 200, body: [] }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/contacts?IncludeEnriched=true`, + status: 200, + body: [{ id: "org1", first_name: "A", last_name: "B", recommended: false }], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/enrich/contacts?IncludeEnriched=true`, + status: 200, + body: [{ id: "paid1", first_name: "C", last_name: "D", recommended: true }], + }, + { + method: "GET", + path: `/1.5/leads/${LEAD}/web_fetch`, + status: 200, + body: { content: null, fetch_at: null }, + }, + ]); + const t = setup(); + const result: any = await executeTool(t, "leadbay_get_lead_profile", { + leadId: LEAD, + lensId: LENS, + }); + const sources = result.contacts.map((c: any) => c.source).sort(); + expect(sources).toEqual(["org", "paid"]); + expect(result.contacts.find((c: any) => c.id === "org1").source).toBe("org"); + expect(result.contacts.find((c: any) => c.id === "paid1").source).toBe("paid"); + }); +}); diff --git a/test/unit/tools/login.test.ts b/test/unit/tools/login.test.ts new file mode 100644 index 00000000..c256e0c7 --- /dev/null +++ b/test/unit/tools/login.test.ts @@ -0,0 +1,173 @@ +/** + * Tests for the login tool. Covers the password-unescape regex — an easy-to-miss + * subtlety, since some LLMs backslash-escape special chars in tool call JSON. + */ + +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + createTestApi, + executeTool, + mockHttp, + resetHttpMock, + httpsMockFactory, +} from "../../harness.js"; + +vi.mock("node:https", () => httpsMockFactory()); + +import { LeadbayClient } from "../../../src/client.js"; +import { registerLogin } from "../../../src/tools/login.js"; + +const BASE = "https://api-us.leadbay.app"; + +beforeEach(() => { + resetHttpMock(); +}); + +describe("leadbay_login — password unescape", () => { + const unescapeCases: Array<[string, string, string]> = [ + ["backslash-escaped special char is stripped", "Pass\\!word", "Pass!word"], + [ + "double backslash collapses to single (known behavior)", + "x\\\\y", + "x\\y", + ], + [ + "trailing lone backslash is preserved (regex needs a follow char)", + "pass\\", + "pass\\", + ], + ]; + + it.each(unescapeCases)( + "%s: %s → %s", + async (_label, input, expected) => { + const { requests } = mockHttp([ + { + method: "POST", + path: "/1.5/auth/login", + status: 200, + body: { token: "u.new-token" }, + }, + // allow the prefetchOrgData fire-and-forget to 404 quietly + { method: "GET", path: /\/1\.5\/users\/me/, status: 404, body: {} }, + ]); + const t = createTestApi({}); + const client = new LeadbayClient(BASE); + registerLogin(t.api as any, client); + + await executeTool(t, "leadbay_login", { + email: "a@b.com", + password: input, + }); + + const loginReq = requests.find((r) => r.path === "/1.5/auth/login"); + expect(loginReq).toBeDefined(); + const payload = JSON.parse(loginReq!.body!); + expect(payload.password).toBe(expected); + } + ); +}); + +describe("leadbay_login — status path handling", () => { + it("200 response sets token on client and returns success", async () => { + mockHttp([ + { + method: "POST", + path: "/1.5/auth/login", + status: 200, + body: { token: "u.abc123" }, + }, + { method: "GET", path: /users\/me/, status: 404, body: {} }, + ]); + const t = createTestApi({}); + const client = new LeadbayClient(BASE); + registerLogin(t.api as any, client); + + const result: any = await executeTool(t, "leadbay_login", { + email: "a@b.com", + password: "secret", + }); + + expect(result).toEqual({ + success: true, + message: "Logged in to Leadbay successfully", + }); + expect(client.isAuthenticated).toBe(true); + }); + + it("401 returns LOGIN_FAILED (does NOT throw)", async () => { + mockHttp([ + { + method: "POST", + path: "/1.5/auth/login", + status: 401, + body: { message: "bad credentials" }, + }, + ]); + const t = createTestApi({}); + const client = new LeadbayClient(BASE); + registerLogin(t.api as any, client); + + const result: any = await executeTool(t, "leadbay_login", { + email: "a@b.com", + password: "wrong", + }); + + expect(result).toMatchObject({ + error: true, + code: "LOGIN_FAILED", + message: "bad credentials", + }); + expect(client.isAuthenticated).toBe(false); + }); + + it("network error returns NETWORK_ERROR", async () => { + mockHttp([ + { + method: "POST", + path: "/1.5/auth/login", + status: 0, + error: new Error("ECONNREFUSED"), + }, + ]); + const t = createTestApi({}); + const client = new LeadbayClient(BASE); + registerLogin(t.api as any, client); + + const result: any = await executeTool(t, "leadbay_login", { + email: "a@b.com", + password: "x", + }); + + expect(result).toMatchObject({ + error: true, + code: "NETWORK_ERROR", + }); + expect(result.message).toContain("ECONNREFUSED"); + }); + + it("prefetchOrgData rejection is swallowed (fire-and-forget)", async () => { + mockHttp([ + { + method: "POST", + path: "/1.5/auth/login", + status: 200, + body: { token: "u.abc" }, + }, + // both prefetch paths fail — login must still resolve cleanly + { method: "GET", path: /users\/me/, status: 500, body: {} }, + ]); + const t = createTestApi({}); + const client = new LeadbayClient(BASE); + registerLogin(t.api as any, client); + + const result: any = await executeTool(t, "leadbay_login", { + email: "a@b.com", + password: "x", + }); + + expect(result.success).toBe(true); + // And no unhandled rejection bubbles up — waiting a tick confirms + await new Promise((r) => setImmediate(r)); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..0cd3b501 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +// Default run: unit + contract + sanity (no network). +// Smoke tests live under test/smoke/ and are run via `npm run test:smoke`. +// They are excluded here and re-included on the CLI for the smoke script. +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"], + exclude: ["test/smoke/**", "node_modules", "dist"], + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: ["src/types.ts"], + reporter: ["text", "html"], + }, + }, +}); diff --git a/vitest.smoke.config.ts b/vitest.smoke.config.ts new file mode 100644 index 00000000..61448594 --- /dev/null +++ b/vitest.smoke.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +// Smoke-only config — used by `npm run test:smoke`. These tests hit the live +// Leadbay API and are opt-in (gated by LEADBAY_TEST_TOKEN env var). +export default defineConfig({ + test: { + environment: "node", + include: ["test/smoke/**/*.test.ts"], + testTimeout: 15_000, + }, +});