diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a908f4a..ccc3396 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,10 @@ "WebFetch(domain:www.npmjs.com)", "WebFetch(domain:docs.stripe.com)", "Bash(npx vitest:*)", - "Bash(mpp-inspector flow:*)" + "Bash(mpp-inspector flow:*)", + "Bash(npx husky:*)", + "Bash(MPP_REAL_ENDPOINT_TESTS=true npm run test:integration)", + "Bash(ls C:/Users/AlexandreBenoit/Documents/Personal/MPP-Inspector/eslint.config.*)" ] } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f0442cf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + groups: + dev-deps: + dependency-type: development + production-deps: + dependency-type: production + open-pull-requests-limit: 10 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e20dfbf..d717c51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,11 @@ on: branches: [main] jobs: - build: + lint-and-unit: runs-on: ubuntu-latest - strategy: matrix: - node-version: [18, 20, 22] + node-version: [20, 22] steps: - uses: actions/checkout@v4 @@ -26,11 +25,69 @@ jobs: - name: Install dependencies run: npm ci - - name: Lint + - name: Lint (typecheck) run: npm run lint - - name: Test - run: npm run test + - name: Lint (eslint) + run: npm run lint:eslint + + - name: Format check + run: npm run format:check + + - name: Unit tests with coverage + run: npm run test:coverage + + - name: Build all packages + run: npm run build:all + + - name: Security audit + run: npm audit --omit=dev --audit-level=high + continue-on-error: true + + integration-mock: + runs-on: ubuntu-latest + needs: lint-and-unit + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build mock server + run: npm run build:mock + + - name: Build CLI + run: npm run build + + - name: Run integration tests (mock server) + run: npm run test:integration + + integration-real: + runs-on: ubuntu-latest + needs: lint-and-unit + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci - - name: Build + - name: Build CLI run: npm run build + + - name: Run integration tests (real mpp.dev endpoint) + run: npm run test:integration:real + env: + MPP_REAL_ENDPOINT_TESTS: "true" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c909748..a610b67 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -115,4 +115,4 @@ jobs: - name: Publish Plugin (depends on CLI being available on npm) run: npm publish --workspace=packages/plugin --provenance --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..71cd71c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist +node_modules +coverage +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4a1222f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..82d1be0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2026-03-27 + +### Added + +- `inspect` command — parse and display 402 Payment Required challenges +- `scan` command — discover MPP endpoints on a domain +- `compare` command — side-by-side pricing comparison across endpoints +- `validate` command — decode and verify receipts and credentials +- `flow` command — dry-run the full MPP payment cycle +- `benchmark` command — load test MPP endpoints (preview) +- `session` command — payment channel testing (preview) +- Spec-compliant challenge parsing (`id`, `realm`, `method`, `request` base64url) +- RFC 9457 Problem Details body parsing with challengeId cross-reference +- Payment method detection: Tempo, Stripe, Lightning, Solana, Card, Custom +- Tempo chain resolution (mainnet 42431, testnet 4218) +- `description` field extraction from both header and Problem Details +- `@mpp-inspector/mock-server` — local mock server with 4 demo endpoints +- `@mpp-inspector/plugin` — Claude Code MCP plugin with 5 tools +- JSON output mode (`--json`) for all commands +- 121 unit tests with vitest +- CI pipeline (GitHub Actions) on Node 18/20/22 diff --git a/README.md b/README.md index b806b6f..c1037f9 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ chain 402 CI + coverage


diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..768941b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,27 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + prettier, + { + ignores: [ + "**/dist/**", + "**/node_modules/**", + "**/*.js", + "**/*.mjs", + ], + }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/consistent-type-imports": "error", + }, + }, +); diff --git a/package-lock.json b/package-lock.json index dc76d0b..b8b781d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,15 @@ "workspaces": [ "packages/*" ], + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", + "prettier": "^3.8.1", + "typescript-eslint": "^8.57.2" + }, "engines": { "node": ">=18.0.0" } @@ -18,6 +27,20 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "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/@apidevtools/json-schema-ref-parser": { "version": "14.2.1", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", @@ -56,6 +79,16 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "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", @@ -65,6 +98,22 @@ "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/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -74,6 +123,30 @@ "node": ">=6.9.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": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -526,6 +599,134 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -538,6 +739,44 @@ "hono": "^4" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/momoa": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", @@ -547,6 +786,91 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@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/@isaacs/cliui/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/@isaacs/cliui/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/@isaacs/cliui/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/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "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", @@ -673,6 +997,17 @@ "url": "https://paulmillr.com/funding/" } }, + "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/@readme/better-ajv-errors": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@readme/better-ajv-errors/-/better-ajv-errors-2.4.0.tgz", @@ -1153,6 +1488,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1176,75 +1518,339 @@ "undici-types": "~7.18.0" } }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, - "funding": { + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "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.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { "url": "https://opencollective.com/vitest" } }, @@ -1338,6 +1944,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -1435,6 +2051,22 @@ "node": ">=8" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1482,6 +2114,35 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "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/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -1528,6 +2189,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -1754,6 +2428,67 @@ "node": ">=8" } }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -1878,6 +2613,13 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1901,6 +2643,13 @@ "node": ">= 0.4" } }, + "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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1922,6 +2671,19 @@ "node": ">= 0.8" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2007,8 +2769,203 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/estree-walker": { - "version": "3.0.3", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, @@ -2017,6 +2974,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2130,6 +3097,20 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -2164,6 +3145,19 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -2185,6 +3179,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fix-dts-default-cjs-exports": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", @@ -2197,6 +3208,44 @@ "rollup": "^4.34.8" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2288,6 +3337,74 @@ "node": ">= 0.4" } }, + "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-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "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/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2300,6 +3417,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2333,6 +3460,13 @@ "node": ">=16.9.0" } }, + "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/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2353,6 +3487,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -2369,6 +3519,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/incur": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/incur/-/incur-0.3.13.tgz", @@ -2420,6 +3590,16 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2429,6 +3609,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -2480,6 +3673,76 @@ "ws": "*" } }, + "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/jose": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", @@ -2518,6 +3781,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2530,6 +3800,13 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -2539,6 +3816,16 @@ "node": ">=0.10.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -2548,6 +3835,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2568,6 +3869,68 @@ "dev": true, "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -2578,6 +3941,22 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -2606,6 +3985,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2613,6 +4045,13 @@ "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", @@ -2623,6 +4062,34 @@ "@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/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2690,6 +4157,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "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/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -2791,6 +4284,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2864,6 +4364,24 @@ "license": "MIT", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -2917,6 +4435,45 @@ } } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2926,13 +4483,40 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { - "node": ">=8" + "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/path-to-regexp": { @@ -3084,6 +4668,32 @@ } } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3097,6 +4707,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -3185,6 +4805,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", @@ -3252,6 +4879,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "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/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -3415,6 +5055,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -3470,6 +5143,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -3487,6 +5170,52 @@ "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", @@ -3502,6 +5231,30 @@ "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/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -3548,6 +5301,34 @@ "node": ">= 6" } }, + "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/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -3657,6 +5438,19 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -3717,6 +5511,19 @@ } } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -3757,6 +5564,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/ufo": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", @@ -3780,6 +5611,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4037,6 +5878,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -4054,6 +5905,86 @@ "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" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4096,6 +6027,19 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -4132,6 +6076,7 @@ }, "devDependencies": { "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^3.2.4", "tsup": "^8.4.0", "typescript": "^5.7.3", "vitest": "^3.0.9" diff --git a/package.json b/package.json index 58c2988..ece047f 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,34 @@ "dev": "npm run dev --workspace=packages/cli", "dev:plugin": "npm run dev --workspace=packages/plugin", "test": "npm run test --workspace=packages/cli", + "test:integration": "npm run test:integration --workspace=packages/cli", + "test:integration:real": "npm run test:integration:real --workspace=packages/cli", "lint": "npm run lint --workspace=packages/cli", "check": "npm run lint && npm run test && npm run build", + "lint:eslint": "eslint packages/*/src/**/*.ts", + "format": "prettier --write \"packages/*/src/**/*.ts\"", + "format:check": "prettier --check \"packages/*/src/**/*.ts\"", + "test:coverage": "npm run test:coverage --workspace=packages/cli", + "prepare": "husky", "mock": "npm run start --workspace=packages/mock-server", "demo": "echo 'Start the mock server first: npm run mock' && echo 'Then in another terminal:' && echo ' npx mpp-inspector inspect http://localhost:3402/v1/query' && echo ' npx mpp-inspector scan localhost:3402'" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", + "prettier": "^3.8.1", + "typescript-eslint": "^8.57.2" + }, + "lint-staged": { + "*.ts": [ + "prettier --write", + "eslint --fix" + ] } } diff --git a/packages/cli/package.json b/packages/cli/package.json index ed0eb44..325072d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -6,8 +6,12 @@ "bin": { "mpp-inspector": "./dist/bin/mpp-inspector.js" }, + "types": "./dist/index.d.ts", "exports": { - ".": "./dist/index.js" + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, "files": [ "dist" @@ -17,7 +21,10 @@ "dev": "tsup --watch", "test": "vitest run", "test:watch": "vitest", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:integration:real": "MPP_REAL_ENDPOINT_TESTS=true vitest run --config vitest.integration.config.ts src/__tests__/integration/real-endpoint.test.ts", "lint": "tsc --noEmit", + "test:coverage": "vitest run --coverage", "prepublishOnly": "npm run lint && npm run test && npm run build" }, "keywords": [ @@ -47,7 +54,7 @@ "url": "https://github.com/AlexandreBenoit/mpp-inspector/issues" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "dependencies": { "boxen": "^8.0.1", @@ -60,6 +67,7 @@ }, "devDependencies": { "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^3.2.4", "tsup": "^8.4.0", "typescript": "^5.7.3", "vitest": "^3.0.9" diff --git a/packages/cli/src/__tests__/integration/mock-server.test.ts b/packages/cli/src/__tests__/integration/mock-server.test.ts new file mode 100644 index 0000000..afeaabc --- /dev/null +++ b/packages/cli/src/__tests__/integration/mock-server.test.ts @@ -0,0 +1,183 @@ +/** + * Integration tests: CLI commands against the mock server. + * + * These tests spin up the mock server, run CLI commands via the + * programmatic API, and verify the parsed output is spec-compliant. + */ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { createMockServer } from "@mpp-inspector/mock-server"; + +// Import CLI functions +import { parseChallengeHeader } from "../../utils/parser.js"; +import { rawRequest } from "../../utils/http.js"; +import { verifyChallengeFields } from "../../utils/crypto.js"; + +let baseUrl: string; +let stopServer: () => Promise; + +beforeAll(async () => { + const mock = createMockServer({ port: 0, silent: true }); + await mock.start(); + const addr = mock.server.address(); + if (addr && typeof addr !== "string") { + baseUrl = `http://127.0.0.1:${addr.port}`; + } + stopServer = mock.stop; +}); + +afterAll(async () => { + await stopServer(); +}); + +describe("mock server health", () => { + it("responds to /health", async () => { + const res = await fetch(`${baseUrl}/health`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.status).toBe("ok"); + }); + + it("serves /.well-known/mpp.json", async () => { + const res = await fetch(`${baseUrl}/.well-known/mpp.json`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.name).toBe("MPP Mock Server"); + expect(body.endpoints).toHaveLength(4); + }); + + it("serves /llms.txt", async () => { + const res = await fetch(`${baseUrl}/llms.txt`); + expect(res.status).toBe(200); + const text = await res.text(); + expect(text).toContain("MPP Mock Server"); + }); +}); + +describe("inspect command against mock endpoints", () => { + it("parses /v1/query as Tempo charge", async () => { + const { status, headers } = await rawRequest(`${baseUrl}/v1/query`); + expect(status).toBe(402); + + const wwwAuth = headers.get("www-authenticate"); + expect(wwwAuth).toBeDefined(); + + const challenge = parseChallengeHeader(wwwAuth!); + expect(challenge.id).toBeTruthy(); + expect(challenge.realm).toBe("mock.mpp-inspector.dev"); + expect(challenge.method).toBe("tempo"); + expect(challenge.intent).toBe("charge"); + expect(challenge.expires).toBeTruthy(); + expect(challenge.request).toBeTruthy(); + + // Decoded request params + expect(challenge.requestDecoded).toBeDefined(); + expect(challenge.requestDecoded!.amount).toBe("1000"); + }); + + it("parses /v1/search as Tempo charge (POST)", async () => { + const { status, headers } = await rawRequest(`${baseUrl}/v1/search`, { + method: "POST", + }); + expect(status).toBe(402); + + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + expect(challenge.method).toBe("tempo"); + expect(challenge.requestDecoded!.amount).toBe("5000"); + }); + + it("parses /v1/stream as Tempo session", async () => { + const { status, headers } = await rawRequest(`${baseUrl}/v1/stream`); + expect(status).toBe(402); + + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + expect(challenge.method).toBe("tempo"); + expect(challenge.intent).toBe("session"); + expect(challenge.requestDecoded!.amount).toBe("100"); + }); + + it("parses /v1/premium as Stripe charge", async () => { + const { status, headers } = await rawRequest(`${baseUrl}/v1/premium`); + expect(status).toBe(402); + + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + expect(challenge.method).toBe("stripe"); + expect(challenge.intent).toBe("charge"); + expect(challenge.requestDecoded!.amount).toBe("500"); + }); + + it("verification passes for all Tempo endpoints", async () => { + for (const path of ["/v1/query", "/v1/search", "/v1/stream"]) { + const { headers } = await rawRequest(`${baseUrl}${path}`, { + method: path === "/v1/search" ? "POST" : "GET", + }); + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + const result = verifyChallengeFields(challenge); + + expect(result.expiryValid).toBe(true); + expect(result.methodKnown).toBe(true); + expect(result.amountParseable).toBe(true); + expect(result.errors).toHaveLength(0); + } + }); +}); + +describe("validate command against mock receipts", () => { + it("validates a receipt from mock server payment", async () => { + // Hit the endpoint with an Authorization header to get a receipt + const res = await fetch(`${baseUrl}/v1/query`, { + headers: { Authorization: "Payment test-credential" }, + }); + + // Mock server returns 200 with receipt when Authorization is present + if (res.status === 200) { + const body = await res.json(); + expect(body.receipt).toBeDefined(); + + // Decode and verify receipt structure + const receiptJson = JSON.parse(Buffer.from(body.receipt, "base64url").toString("utf-8")); + expect(receiptJson.challengeId).toBeTruthy(); + expect(receiptJson.method).toBe("tempo"); + expect(receiptJson.settlement).toBeDefined(); + expect(receiptJson.settlement.amount).toBe("1000"); + expect(receiptJson.status).toBe("success"); + } + }); +}); + +describe("scan discovers mock server endpoints", () => { + it("finds /.well-known/mpp.json and /llms.txt", async () => { + // Test the manifest endpoint directly + const mppRes = await fetch(`${baseUrl}/.well-known/mpp.json`); + expect(mppRes.status).toBe(200); + + const manifest = await mppRes.json(); + expect(manifest.endpoints.length).toBeGreaterThanOrEqual(4); + + // Verify each endpoint in manifest returns 402 + for (const ep of manifest.endpoints) { + const epRes = await fetch(`${baseUrl}${ep.path}`, { + method: ep.method, + }); + expect(epRes.status).toBe(402); + expect(epRes.headers.get("www-authenticate")).toContain("Payment"); + } + }); +}); + +describe("compare across mock endpoints", () => { + it("all endpoints return parseable challenges", async () => { + const paths = ["/v1/query", "/v1/premium"]; + const results = await Promise.all( + paths.map(async (path) => { + const { headers } = await rawRequest(`${baseUrl}${path}`); + return parseChallengeHeader(headers.get("www-authenticate")!); + }), + ); + + expect(results).toHaveLength(2); + expect(results[0].requestDecoded!.amount).toBe("1000"); + expect(results[1].requestDecoded!.amount).toBe("500"); + expect(results[0].method).toBe("tempo"); + expect(results[1].method).toBe("stripe"); + }); +}); diff --git a/packages/cli/src/__tests__/integration/real-endpoint.test.ts b/packages/cli/src/__tests__/integration/real-endpoint.test.ts new file mode 100644 index 0000000..330ef5e --- /dev/null +++ b/packages/cli/src/__tests__/integration/real-endpoint.test.ts @@ -0,0 +1,87 @@ +/** + * Integration tests against the real MPP endpoint at mpp.dev. + * + * These tests verify our parser works against production MPP challenges. + * They require network access and are skipped if MPP_REAL_ENDPOINT_TESTS + * is not set (to avoid flaky CI on network issues). + */ +import { describe, it, expect } from "vitest"; +import { parseChallengeHeader } from "../../utils/parser.js"; +import { verifyChallengeFields } from "../../utils/crypto.js"; +import { rawRequest } from "../../utils/http.js"; + +const REAL_ENDPOINT = "https://mpp.dev/api/ping/paid"; +const shouldRun = process.env.MPP_REAL_ENDPOINT_TESTS === "true"; + +describe.skipIf(!shouldRun)("real endpoint: mpp.dev/api/ping/paid", () => { + it("returns 402 with WWW-Authenticate header", async () => { + const { status, headers } = await rawRequest(REAL_ENDPOINT, { + timeout: 15_000, + }); + + expect(status).toBe(402); + expect(headers.get("www-authenticate")).toBeDefined(); + expect(headers.get("www-authenticate")).toContain("Payment"); + }); + + it("parses a spec-compliant challenge", async () => { + const { headers } = await rawRequest(REAL_ENDPOINT, { timeout: 15_000 }); + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + + // Core spec fields + expect(challenge.id).toBeTruthy(); + expect(challenge.id!.length).toBeGreaterThan(10); + expect(challenge.realm).toBeTruthy(); + expect(challenge.method).toBe("tempo"); + expect(challenge.intent).toBe("charge"); + expect(challenge.expires).toBeTruthy(); + expect(challenge.request).toBeTruthy(); + }); + + it("decodes the request payload correctly", async () => { + const { headers } = await rawRequest(REAL_ENDPOINT, { timeout: 15_000 }); + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + + expect(challenge.requestDecoded).toBeDefined(); + expect(challenge.requestDecoded!.amount).toBeTruthy(); + expect(challenge.requestDecoded!.currency).toBeTruthy(); + expect(challenge.requestDecoded!.recipient).toBeTruthy(); + + // Tempo-specific: methodDetails with chainId + expect(challenge.requestDecoded!.methodDetails).toBeDefined(); + const details = challenge.requestDecoded!.methodDetails as Record; + expect(details.chainId).toBe(42431); + }); + + it("passes verification checks", async () => { + const { headers } = await rawRequest(REAL_ENDPOINT, { timeout: 15_000 }); + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + const result = verifyChallengeFields(challenge); + + expect(result.expiryValid).toBe(true); + expect(result.methodKnown).toBe(true); + expect(result.amountParseable).toBe(true); + expect(result.recipientValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("returns RFC 9457 Problem Details body", async () => { + const res = await fetch(REAL_ENDPOINT); + expect(res.status).toBe(402); + + const body = await res.json(); + expect(body.type).toContain("paymentauth.org"); + expect(body.title).toBe("Payment Required"); + expect(body.status).toBe(402); + expect(body.detail).toBeTruthy(); + expect(body.challengeId).toBeTruthy(); + }); + + it("description field is present in challenge header", async () => { + const { headers } = await rawRequest(REAL_ENDPOINT, { timeout: 15_000 }); + const challenge = parseChallengeHeader(headers.get("www-authenticate")!); + + expect(challenge.description).toBeTruthy(); + expect(challenge.description).toContain("Ping"); + }); +}); diff --git a/packages/cli/src/__tests__/utils/crypto.test.ts b/packages/cli/src/__tests__/utils/crypto.test.ts index b208f9e..8943a69 100644 --- a/packages/cli/src/__tests__/utils/crypto.test.ts +++ b/packages/cli/src/__tests__/utils/crypto.test.ts @@ -2,7 +2,10 @@ import { describe, it, expect } from "vitest"; import { verifyChallengeFields } from "../../utils/crypto.js"; import type { MppChallenge, MppRequestParams } from "../../types.js"; -function makeChallenge(overrides: Partial = {}, reqOverrides: Partial = {}): MppChallenge { +function makeChallenge( + overrides: Partial = {}, + reqOverrides: Partial = {}, +): MppChallenge { const requestDecoded: MppRequestParams = { amount: "0.001", currency: "usd", @@ -103,4 +106,46 @@ describe("verifyChallengeFields", () => { ); expect(result.errors.length).toBeGreaterThanOrEqual(3); }); + + it("reports error for invalid expires format", () => { + const result = verifyChallengeFields(makeChallenge({ expires: "not-a-date" })); + expect(result.expiryValid).toBe(false); + expect(result.errors.some((e) => e.includes("Invalid expires format"))).toBe(true); + }); + + it("reports error for missing expires field", () => { + const result = verifyChallengeFields(makeChallenge({ expires: "" })); + expect(result.expiryValid).toBe(false); + expect(result.errors.some((e) => e.includes("Missing expires"))).toBe(true); + }); + + it("reports error for missing payment method", () => { + const result = verifyChallengeFields(makeChallenge({ method: "" })); + expect(result.methodKnown).toBe(false); + expect(result.errors.some((e) => e.includes("Missing payment method"))).toBe(true); + }); + + it("treats amount as parseable when request exists but amount is missing", () => { + const result = verifyChallengeFields( + makeChallenge({ request: "some-base64-data" }, { amount: undefined }), + ); + expect(result.amountParseable).toBe(true); + }); + + it("returns currencyKnown true for hex address (resolved to truncated display)", () => { + // An unknown hex token address still resolves (truncated), so currencyKnown = true + const unknownToken = "0x" + "ab".repeat(40); + const result = verifyChallengeFields(makeChallenge({}, { currency: unknownToken })); + expect(result.currencyKnown).toBe(true); + }); + + it("returns currencyKnown null when currency is absent", () => { + const result = verifyChallengeFields(makeChallenge({}, { currency: undefined })); + expect(result.currencyKnown).toBeNull(); + }); + + it("returns currencyKnown true for known fiat currency", () => { + const result = verifyChallengeFields(makeChallenge({}, { currency: "usd" })); + expect(result.currencyKnown).toBe(true); + }); }); diff --git a/packages/cli/src/__tests__/utils/format.test.ts b/packages/cli/src/__tests__/utils/format.test.ts index 43dc889..8aa5060 100644 --- a/packages/cli/src/__tests__/utils/format.test.ts +++ b/packages/cli/src/__tests__/utils/format.test.ts @@ -6,6 +6,7 @@ import { formatUsd, formatDuration, formatExpiry, + formatChainName, formatPaymentMethod, progressBar, check, @@ -101,6 +102,71 @@ describe("formatDuration", () => { }); }); +describe("formatExpiry", () => { + it("returns 'none' for empty expires", () => { + const result = formatExpiry(""); + expect(result).toContain("none"); + }); + + it("returns 'expired' for past date", () => { + const past = new Date(Date.now() - 60_000).toISOString(); + const result = formatExpiry(past); + expect(result).toContain("expired"); + }); + + it("returns remaining time for future date", () => { + const future = new Date(Date.now() + 300_000).toISOString(); // 5 minutes + const result = formatExpiry(future); + expect(result).toContain("remaining"); + expect(result).toContain("m"); + }); + + it("returns raw string for invalid date", () => { + const result = formatExpiry("not-a-date"); + expect(result).toContain("not-a-date"); + }); +}); + +describe("formatChainName", () => { + it("returns chain name with ID for known chain", () => { + const result = formatChainName(42431); + expect(result).toContain("42431"); + }); + + it("returns N/A for undefined", () => { + expect(formatChainName(undefined)).toBe("N/A"); + }); + + it("returns N/A for zero", () => { + expect(formatChainName(0)).toBe("N/A"); + }); + + it("returns fallback for unknown chain", () => { + const result = formatChainName(99999); + expect(result).toContain("99999"); + }); +}); + +describe("formatPaymentMethod", () => { + it("capitalizes known methods", () => { + expect(formatPaymentMethod("tempo")).toBe("Tempo"); + expect(formatPaymentMethod("stripe")).toBe("Stripe"); + expect(formatPaymentMethod("lightning")).toBe("Lightning"); + expect(formatPaymentMethod("solana")).toBe("Solana"); + expect(formatPaymentMethod("card")).toBe("Card"); + expect(formatPaymentMethod("custom")).toBe("Custom"); + }); + + it("handles case-insensitive input", () => { + expect(formatPaymentMethod("TEMPO")).toBe("Tempo"); + expect(formatPaymentMethod("Stripe")).toBe("Stripe"); + }); + + it("returns raw string for unknown methods", () => { + expect(formatPaymentMethod("unknown-method")).toBe("unknown-method"); + }); +}); + describe("progressBar", () => { it("shows full bar at 100%", () => { const result = progressBar(10, 10, 10); diff --git a/packages/cli/src/__tests__/utils/parser.test.ts b/packages/cli/src/__tests__/utils/parser.test.ts index 6e33465..b8a9d8f 100644 --- a/packages/cli/src/__tests__/utils/parser.test.ts +++ b/packages/cli/src/__tests__/utils/parser.test.ts @@ -1,9 +1,15 @@ import { describe, it, expect } from "vitest"; -import { parseAuthParams, parseChallengeHeader, decodeReceipt, parseMppManifest, decodeCredential } from "../../utils/parser.js"; +import { + parseAuthParams, + parseChallengeHeader, + decodeReceipt, + parseMppManifest, + decodeCredential, +} from "../../utils/parser.js"; describe("parseAuthParams", () => { it("parses simple key=value pairs", () => { - const result = parseAuthParams('intent=charge, amount=0.001'); + const result = parseAuthParams("intent=charge, amount=0.001"); expect(result).toEqual({ intent: "charge", amount: "0.001" }); }); @@ -221,6 +227,29 @@ describe("decodeReceipt — spec-compliant format", () => { }); }); +describe("decodeReceipt — numeric timestamp", () => { + function encode(obj: Record): string { + return Buffer.from(JSON.stringify(obj)).toString("base64url"); + } + + it("converts numeric timestamp to ISO string", () => { + const unixSeconds = Math.floor(Date.now() / 1000) - 10; + const input = encode({ + challengeId: "ch-num", + method: "tempo", + reference: "0xref", + settlement: { amount: "500", currency: "usd" }, + status: "success", + timestamp: unixSeconds, + }); + const { receipt, validation } = decodeReceipt(input); + + expect(validation.timestampValid).toBe(true); + expect(receipt.timestamp).toContain("T"); // ISO 8601 format + expect(receipt.timestamp).toContain("Z"); + }); +}); + describe("decodeCredential", () => { function encode(obj: Record): string { return Buffer.from(JSON.stringify(obj)).toString("base64url"); @@ -228,7 +257,13 @@ describe("decodeCredential", () => { it("decodes a valid spec-compliant credential", () => { const input = encode({ - challenge: { id: "ch1", realm: "example.com", method: "tempo", intent: "charge", request: "base64data" }, + challenge: { + id: "ch1", + realm: "example.com", + method: "tempo", + intent: "charge", + request: "base64data", + }, source: "0x1234567890abcdef", payload: { signature: "0xsig" }, }); @@ -277,6 +312,38 @@ describe("decodeCredential", () => { expect(validation.jsonValid).toBe(true); expect(validation.structureValid).toBe(true); }); + + it("returns invalid base64 error for totally invalid input", () => { + // A string that isn't valid base64 AND isn't valid JSON + const { validation } = decodeCredential("!!!not-base64-not-json!!!"); + expect(validation.jsonValid).toBe(false); + expect(validation.errors.some((e) => e.includes("Invalid JSON"))).toBe(true); + }); + + it("handles base64 that decodes to non-JSON and raw is also not JSON", () => { + // Valid base64 but decodes to non-JSON, and the raw input is also not JSON + const notJson = Buffer.from("this is not json").toString("base64url"); + const { validation } = decodeCredential(notJson); + expect(validation.base64Valid).toBe(true); + expect(validation.jsonValid).toBe(false); + expect(validation.errors.some((e) => e.includes("Invalid JSON"))).toBe(true); + }); + + it("handles base64 that decodes to JSON array (not object — but Array is typeof object)", () => { + const arrayJson = Buffer.from("[1,2,3]").toString("base64url"); + const { credential, validation } = decodeCredential(arrayJson); + // Arrays pass typeof "object" check, so jsonValid = true, but structure is invalid + expect(validation.jsonValid).toBe(true); + expect(validation.structureValid).toBe(false); + expect(credential.challenge.id).toBe(""); + }); + + it("returns empty credential structure on all failures", () => { + const { credential } = decodeCredential("garbage"); + expect(credential.challenge.id).toBe(""); + expect(credential.source).toBe(""); + expect(credential.raw).toBe("garbage"); + }); }); describe("parseMppManifest", () => { diff --git a/packages/cli/src/__tests__/utils/problem-details.test.ts b/packages/cli/src/__tests__/utils/problem-details.test.ts new file mode 100644 index 0000000..ee6d021 --- /dev/null +++ b/packages/cli/src/__tests__/utils/problem-details.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from "vitest"; +import { parseProblemDetails } from "../../utils/parser.js"; + +describe("parseProblemDetails", () => { + it("parses a spec-compliant RFC 9457 problem details body", () => { + const body = JSON.stringify({ + type: "https://paymentauth.org/problems/payment-required", + title: "Payment Required", + status: 402, + detail: "Payment is required (Ping endpoint access).", + challengeId: "abc123", + }); + const result = parseProblemDetails(body); + + expect(result).not.toBeNull(); + expect(result!.type).toBe("https://paymentauth.org/problems/payment-required"); + expect(result!.title).toBe("Payment Required"); + expect(result!.status).toBe(402); + expect(result!.detail).toBe("Payment is required (Ping endpoint access)."); + expect(result!.challengeId).toBe("abc123"); + }); + + it("accepts body with status: 402 but no type field", () => { + const body = JSON.stringify({ + status: 402, + title: "Payment Required", + detail: "Pay up", + }); + const result = parseProblemDetails(body); + + expect(result).not.toBeNull(); + expect(result!.status).toBe(402); + expect(result!.type).toBeUndefined(); + }); + + it("returns null for non-problem-details JSON (no type, wrong status)", () => { + const body = JSON.stringify({ error: "not found", status: 404 }); + const result = parseProblemDetails(body); + expect(result).toBeNull(); + }); + + it("returns null for non-JSON string", () => { + const result = parseProblemDetails("not json at all"); + expect(result).toBeNull(); + }); + + it("returns null for JSON array", () => { + const result = parseProblemDetails("[1,2,3]"); + expect(result).toBeNull(); + }); + + it("returns null for JSON null", () => { + const result = parseProblemDetails("null"); + expect(result).toBeNull(); + }); + + it("collects unknown fields into extra", () => { + const body = JSON.stringify({ + type: "https://example.com/problem", + title: "Error", + customField: "custom-value", + another: 42, + }); + const result = parseProblemDetails(body); + + expect(result).not.toBeNull(); + expect(result!.extra).toEqual({ customField: "custom-value", another: 42 }); + }); + + it("handles missing optional fields gracefully", () => { + const body = JSON.stringify({ + type: "https://example.com/problem", + }); + const result = parseProblemDetails(body); + + expect(result).not.toBeNull(); + expect(result!.title).toBeUndefined(); + expect(result!.status).toBeUndefined(); + expect(result!.detail).toBeUndefined(); + expect(result!.challengeId).toBeUndefined(); + }); + + it("ignores non-string type/title/detail fields", () => { + const body = JSON.stringify({ + type: 123, + title: true, + detail: [], + status: 402, + }); + const result = parseProblemDetails(body); + + expect(result).not.toBeNull(); + expect(result!.type).toBeUndefined(); + expect(result!.title).toBeUndefined(); + expect(result!.detail).toBeUndefined(); + expect(result!.status).toBe(402); + }); +}); diff --git a/packages/cli/src/__tests__/utils/wallet.test.ts b/packages/cli/src/__tests__/utils/wallet.test.ts index a4e6005..c1cdd68 100644 --- a/packages/cli/src/__tests__/utils/wallet.test.ts +++ b/packages/cli/src/__tests__/utils/wallet.test.ts @@ -1,5 +1,10 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { resolvePrivateKey, createMppWallet, NonceManager } from "../../utils/wallet.js"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import { + resolvePrivateKey, + createMppWallet, + getBalance, + NonceManager, +} from "../../utils/wallet.js"; describe("resolvePrivateKey", () => { const originalEnv = process.env.MPP_PRIVATE_KEY; @@ -53,6 +58,32 @@ describe("createMppWallet", () => { }); }); +describe("getBalance", () => { + it("returns formatted balance on success", async () => { + const fakeWallet = { + publicClient: { + getBalance: vi.fn().mockResolvedValue(1000000000000000000n), // 1 ETH in wei + }, + account: { address: "0x1234" }, + } as any; + + const result = await getBalance(fakeWallet); + expect(result).toBe("1"); + }); + + it("returns 'unknown' when getBalance throws", async () => { + const fakeWallet = { + publicClient: { + getBalance: vi.fn().mockRejectedValue(new Error("network error")), + }, + account: { address: "0x1234" }, + } as any; + + const result = await getBalance(fakeWallet); + expect(result).toBe("unknown"); + }); +}); + describe("NonceManager", () => { it("increments nonce on sequential calls", async () => { const mockGetTransactionCount = vi.fn().mockResolvedValue(5); @@ -74,4 +105,35 @@ describe("NonceManager", () => { expect(n3).toBe(7); expect(mockGetTransactionCount).toHaveBeenCalledOnce(); }); + + it("queues concurrent callers while fetching initial nonce", async () => { + let resolveFetch: (value: number) => void; + const fetchPromise = new Promise((resolve) => { + resolveFetch = resolve; + }); + const mockGetTransactionCount = vi.fn().mockReturnValue(fetchPromise); + const fakeWallet = { + publicClient: { + getTransactionCount: mockGetTransactionCount, + }, + account: { address: "0x1234" }, + } as any; + + const manager = new NonceManager(fakeWallet); + + // Start 3 concurrent acquireNonce calls + const p1 = manager.acquireNonce(); + const p2 = manager.acquireNonce(); + const p3 = manager.acquireNonce(); + + // Resolve the initial fetch + resolveFetch!(10); + + const [n1, n2, n3] = await Promise.all([p1, p2, p3]); + + // Pending waiters (p2, p3) get nonces first (10, 11), then p1 gets 12 + const nonces = [n1, n2, n3].sort((a, b) => a - b); + expect(nonces).toEqual([10, 11, 12]); + expect(mockGetTransactionCount).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/cli/src/commands/compare.ts b/packages/cli/src/commands/compare.ts index 901faa8..0d7690b 100644 --- a/packages/cli/src/commands/compare.ts +++ b/packages/cli/src/commands/compare.ts @@ -1,10 +1,8 @@ import { Command } from "commander"; -import chalk from "chalk"; import { rawRequest } from "../utils/http.js"; import { parseChallengeHeader } from "../utils/parser.js"; import { displayPriceComparison, compareToJson } from "../display/table.js"; import { resolveCurrency, getChainName } from "../utils/chains.js"; -import { formatPaymentMethod } from "../utils/format.js"; import type { CompareEntry } from "../types.js"; export const compareCommand = new Command("compare") diff --git a/packages/cli/src/commands/flow.ts b/packages/cli/src/commands/flow.ts index 2450534..5cdd02c 100644 --- a/packages/cli/src/commands/flow.ts +++ b/packages/cli/src/commands/flow.ts @@ -5,7 +5,12 @@ import { rawRequest } from "../utils/http.js"; import { parseChallengeHeader } from "../utils/parser.js"; import { verifyChallengeFields } from "../utils/crypto.js"; import { createMppWallet, getBalance, resolvePrivateKey } from "../utils/wallet.js"; -import { displayFlowHeader, displayFlowStep, displayFlowSummary, flowToJson } from "../display/flow.js"; +import { + displayFlowHeader, + displayFlowStep, + displayFlowSummary, + flowToJson, +} from "../display/flow.js"; import { truncateAddress, formatAmount, formatPaymentMethod } from "../utils/format.js"; import type { FlowStep } from "../types.js"; @@ -22,7 +27,9 @@ export const flowCommand = new Command("flow") .action(async (url: string, options) => { const privateKey = resolvePrivateKey(options.wallet); if (!privateKey && !options.dryRun) { - console.error("Error: Provide --wallet or set MPP_PRIVATE_KEY env var (use --dry-run to skip payment)"); + console.error( + "Error: Provide --wallet or set MPP_PRIVATE_KEY env var (use --dry-run to skip payment)", + ); process.exit(1); } @@ -46,9 +53,13 @@ export const flowCommand = new Command("flow") if (response.status !== 402) { if (options.json) { - console.log(JSON.stringify(flowToJson(steps, { error: `Expected 402, got ${response.status}` }))); + console.log( + JSON.stringify(flowToJson(steps, { error: `Expected 402, got ${response.status}` })), + ); } else { - console.log(`\n ${chalk.red("!")} Expected 402, got ${response.status}. Cannot proceed with flow.`); + console.log( + `\n ${chalk.red("!")} Expected 402, got ${response.status}. Cannot proceed with flow.`, + ); } return; } @@ -80,9 +91,7 @@ export const flowCommand = new Command("flow") const chainId = options.testnet ? 4218 : (req?.chainId ?? 4217); if (options.dryRun) { - const costDesc = req?.amount - ? formatAmount(req.amount, req.currency) - : "unknown"; + const costDesc = req?.amount ? formatAmount(req.amount, req.currency) : "unknown"; steps.push({ name: "Sign transaction (dry-run)", @@ -177,7 +186,11 @@ export const flowCommand = new Command("flow") }; console.log(JSON.stringify(flowToJson(steps, summary), null, 2)); } else if (options.dryRun) { - displayFlowHeader(url, options.wallet ? truncateAddress(options.wallet) : "(dry-run)", undefined); + displayFlowHeader( + url, + options.wallet ? truncateAddress(options.wallet) : "(dry-run)", + undefined, + ); for (let i = 0; i < steps.length; i++) { displayFlowStep(steps[i], i, steps.length); } diff --git a/packages/cli/src/commands/inspect.ts b/packages/cli/src/commands/inspect.ts index 17fc60e..248d6a4 100644 --- a/packages/cli/src/commands/inspect.ts +++ b/packages/cli/src/commands/inspect.ts @@ -24,10 +24,17 @@ export const inspectCommand = new Command("inspect") if (response.status !== 402) { if (options.json) { - console.log(JSON.stringify({ error: `Server returned ${response.status}, not 402`, status: response.status })); + console.log( + JSON.stringify({ + error: `Server returned ${response.status}, not 402`, + status: response.status, + }), + ); return; } - console.log(`\n ${chalk.yellow("!")} Server returned ${chalk.bold(String(response.status))}, not 402.`); + console.log( + `\n ${chalk.yellow("!")} Server returned ${chalk.bold(String(response.status))}, not 402.`, + ); console.log(` This endpoint may not be MPP-enabled.`); if (response.status === 200) { console.log(` The endpoint returned content without requiring payment.`); @@ -38,10 +45,14 @@ export const inspectCommand = new Command("inspect") const wwwAuth = response.headers.get("www-authenticate"); if (!wwwAuth || !wwwAuth.toLowerCase().startsWith("payment ")) { if (options.json) { - console.log(JSON.stringify({ error: "402 returned but no WWW-Authenticate: Payment header found" })); + console.log( + JSON.stringify({ error: "402 returned but no WWW-Authenticate: Payment header found" }), + ); return; } - console.log(`\n ${chalk.yellow("!")} 402 returned but no ${chalk.bold("WWW-Authenticate: Payment")} header found.`); + console.log( + `\n ${chalk.yellow("!")} 402 returned but no ${chalk.bold("WWW-Authenticate: Payment")} header found.`, + ); console.log(" This may be a standard 402, not MPP."); return; } @@ -51,7 +62,9 @@ export const inspectCommand = new Command("inspect") const problemDetails = parseProblemDetails(response.body); if (options.json) { - console.log(JSON.stringify(challengeToJson(url, challenge, verification, problemDetails), null, 2)); + console.log( + JSON.stringify(challengeToJson(url, challenge, verification, problemDetails), null, 2), + ); } else { displayChallenge(url, challenge, verification, problemDetails); } diff --git a/packages/cli/src/commands/scan.ts b/packages/cli/src/commands/scan.ts index 8f70aaf..d712f4d 100644 --- a/packages/cli/src/commands/scan.ts +++ b/packages/cli/src/commands/scan.ts @@ -14,9 +14,10 @@ export const scanCommand = new Command("scan") .option("--timeout ", "Request timeout in milliseconds", "10000") .action(async (domain: string, options) => { const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/+$/, ""); - const baseUrl = cleanDomain.includes("localhost") || cleanDomain.includes("127.0.0.1") - ? `http://${cleanDomain}` - : `https://${cleanDomain}`; + const baseUrl = + cleanDomain.includes("localhost") || cleanDomain.includes("127.0.0.1") + ? `http://${cleanDomain}` + : `https://${cleanDomain}`; const endpoints: MppEndpoint[] = []; const timeout = parseInt(options.timeout); @@ -29,7 +30,9 @@ export const scanCommand = new Command("scan") try { const { data, status } = await fetchJson(`${baseUrl}/.well-known/mpp.json`, timeout); if (status === 200 && data) { - manifestSpinner?.succeed(` Checking /.well-known/mpp.json ${chalk.green("\u2713 Found")}`); + manifestSpinner?.succeed( + ` Checking /.well-known/mpp.json ${chalk.green("\u2713 Found")}`, + ); const manifest = parseMppManifest(data); for (const ep of manifest.endpoints) { endpoints.push(ep); @@ -59,9 +62,13 @@ export const scanCommand = new Command("scan") try { const response = await rawRequest(`${baseUrl}/health`, { timeout }); if (response.status === 200) { - healthSpinner?.succeed(` Checking /health ${chalk.green("\u2713 200 OK")}`); + healthSpinner?.succeed( + ` Checking /health ${chalk.green("\u2713 200 OK")}`, + ); } else { - healthSpinner?.info(` Checking /health ${chalk.dim(String(response.status))}`); + healthSpinner?.info( + ` Checking /health ${chalk.dim(String(response.status))}`, + ); } } catch { healthSpinner?.fail(` Checking /health ${chalk.dim("Error")}`); @@ -69,10 +76,7 @@ export const scanCommand = new Command("scan") // Probe common API paths if (options.probe) { - const probePaths = [ - "/v1/", "/api/", "/api/v1/", - "/v1/search", "/v1/data", "/v1/query", - ]; + const probePaths = ["/v1/", "/api/", "/api/v1/", "/v1/search", "/v1/data", "/v1/query"]; for (const path of probePaths) { try { diff --git a/packages/cli/src/commands/session.ts b/packages/cli/src/commands/session.ts index 5283ac1..0fecab8 100644 --- a/packages/cli/src/commands/session.ts +++ b/packages/cli/src/commands/session.ts @@ -34,11 +34,14 @@ export const sessionCommand = new Command("session") if (!options.json) { console.log( "\n" + - boxen(` ${chalk.bold("MPP Session Test")}\n ${chalk.dim("Testing payment channel lifecycle")}`, { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderStyle: "round", - borderColor: "cyan", - }), + boxen( + ` ${chalk.bold("MPP Session Test")}\n ${chalk.dim("Testing payment channel lifecycle")}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderStyle: "round", + borderColor: "cyan", + }, + ), ); } @@ -59,8 +62,12 @@ export const sessionCommand = new Command("session") if (challenge.intent !== "session") { if (!options.json) { - console.log(`\n ${chalk.yellow("!")} Endpoint uses "${challenge.intent}" intent, not "session".`); - console.log(` Session channels require intent="session". This endpoint uses one-time charges.`); + console.log( + `\n ${chalk.yellow("!")} Endpoint uses "${challenge.intent}" intent, not "session".`, + ); + console.log( + ` Session channels require intent="session". This endpoint uses one-time charges.`, + ); console.log(` Simulating session behavior with repeated charges instead.\n`); } } @@ -71,7 +78,9 @@ export const sessionCommand = new Command("session") const step1Time = performance.now() - step1Start; if (!options.json) { - console.log(`\n ${chalk.green("\u2713")} ${chalk.bold("Step 1")} \u2500 Open channel ${chalk.dim(`[${formatDuration(step1Time)}]`)}`); + console.log( + `\n ${chalk.green("\u2713")} ${chalk.bold("Step 1")} \u2500 Open channel ${chalk.dim(`[${formatDuration(step1Time)}]`)}`, + ); console.log(` Deposit: ${deposit} \u2192 escrow contract`); console.log(` Wallet: ${truncateAddress(wallet.address)} (balance: ${balance})`); } @@ -119,7 +128,9 @@ export const sessionCommand = new Command("session") const step3Time = performance.now() - step3Start; if (!options.json) { - console.log(`\n ${chalk.green("\u2713")} ${chalk.bold("Step 3")} \u2500 Close channel ${chalk.dim(`[${formatDuration(step3Time)}]`)}`); + console.log( + `\n ${chalk.green("\u2713")} ${chalk.bold("Step 3")} \u2500 Close channel ${chalk.dim(`[${formatDuration(step3Time)}]`)}`, + ); console.log(` Final settlement: ${deposit} to recipient`); } diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 856ce92..0a075f3 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -1,7 +1,12 @@ import { Command } from "commander"; import { readFileSync } from "node:fs"; import { decodeReceipt, decodeCredential } from "../utils/parser.js"; -import { displayReceipt, displayCredential, receiptToJson, credentialToJson } from "../display/receipt.js"; +import { + displayReceipt, + displayCredential, + receiptToJson, + credentialToJson, +} from "../display/receipt.js"; export const validateCommand = new Command("validate") .description("Validate a receipt or credential") diff --git a/packages/cli/src/display/challenge.ts b/packages/cli/src/display/challenge.ts index f6118cc..b461e33 100644 --- a/packages/cli/src/display/challenge.ts +++ b/packages/cli/src/display/challenge.ts @@ -34,11 +34,12 @@ export function displayChallenge( console.log(label("Protocol:", "MPP (Payment HTTP Authentication Scheme)")); const req = challenge.requestDecoded; - const intentDesc = challenge.intent === "charge" - ? "charge (one-time payment)" - : challenge.intent === "session" - ? "session (streaming/prepaid)" - : challenge.intent; + const intentDesc = + challenge.intent === "charge" + ? "charge (one-time payment)" + : challenge.intent === "session" + ? "session (streaming/prepaid)" + : challenge.intent; const currencyDisplay = req?.currency ? resolveCurrency(req.currency) : "N/A"; const amountDisplay = req?.amount @@ -49,7 +50,10 @@ export function displayChallenge( const description = challenge.description ?? problemDetails?.detail; const details = [ - label("Challenge ID:", challenge.id ? truncateAddress(challenge.id, 12, 0) : chalk.dim("(none)")), + label( + "Challenge ID:", + challenge.id ? truncateAddress(challenge.id, 12, 0) : chalk.dim("(none)"), + ), label("Realm:", challenge.realm || chalk.dim("(none)")), label("Method:", formatPaymentMethod(challenge.method) || chalk.dim("(none)")), label("Intent:", intentDesc), @@ -114,10 +118,16 @@ export function displayChallenge( const checks = [ check(verification.signatureValid, "Challenge signature valid"), check(verification.expiryValid, "Expiry in future"), - check(verification.methodKnown, `Payment method known (${formatPaymentMethod(challenge.method)})`), + check( + verification.methodKnown, + `Payment method known (${formatPaymentMethod(challenge.method)})`, + ), check(verification.amountParseable, "Amount parseable"), check(verification.recipientValid, "Recipient valid"), - check(verification.currencyKnown, "Currency recognized" + (currencyDisplay !== "N/A" ? ` (${currencyDisplay})` : "")), + check( + verification.currencyKnown, + "Currency recognized" + (currencyDisplay !== "N/A" ? ` (${currencyDisplay})` : ""), + ), ].join("\n"); console.log("\n" + section("Verification", checks)); @@ -133,7 +143,9 @@ export function displayChallenge( const methodDesc = methodInfo ? methodInfo.description : `${challenge.method} payment`; console.log(label("Payment method:", methodDesc)); if (req?.amount) { - console.log(label("Estimated cost:", `$${req.amount} USD${methodInfo?.blockchain ? " + gas" : ""}`)); + console.log( + label("Estimated cost:", `$${req.amount} USD${methodInfo?.blockchain ? " + gas" : ""}`), + ); } console.log(); } diff --git a/packages/cli/src/display/flow.ts b/packages/cli/src/display/flow.ts index 76f93fa..7a05e60 100644 --- a/packages/cli/src/display/flow.ts +++ b/packages/cli/src/display/flow.ts @@ -4,7 +4,11 @@ import type { FlowStep } from "../types.js"; import { formatDuration, label, section } from "../utils/format.js"; export function displayFlowHeader(url: string, walletAddress: string, balance?: string): void { - const lines = [` ${chalk.bold("MPP Payment Flow")}`, ` ${chalk.dim("URL:")} ${url}`, ` ${chalk.dim("Wallet:")} ${walletAddress}`]; + const lines = [ + ` ${chalk.bold("MPP Payment Flow")}`, + ` ${chalk.dim("URL:")} ${url}`, + ` ${chalk.dim("Wallet:")} ${walletAddress}`, + ]; if (balance) lines.push(` ${chalk.dim("Balance:")} ${balance}`); console.log( @@ -20,10 +24,16 @@ export function displayFlowHeader(url: string, walletAddress: string, balance?: export function displayFlowStep(step: FlowStep, index: number, total: number): void { const timing = chalk.dim(`[${formatDuration(step.timing)}]`); const statusIcon = - step.status === "success" ? chalk.green("\u2713") : step.status === "failure" ? chalk.red("\u2717") : chalk.yellow("-"); + step.status === "success" + ? chalk.green("\u2713") + : step.status === "failure" + ? chalk.red("\u2717") + : chalk.yellow("-"); console.log(); - console.log(` ${statusIcon} ${chalk.bold(`Step ${index + 1}/${total}`)} \u2500 ${step.name} ${timing}`); + console.log( + ` ${statusIcon} ${chalk.bold(`Step ${index + 1}/${total}`)} \u2500 ${step.name} ${timing}`, + ); for (const [key, value] of Object.entries(step.details)) { if (value !== undefined && value !== null) { @@ -32,9 +42,14 @@ export function displayFlowStep(step: FlowStep, index: number, total: number): v } } -export function displayFlowSummary(steps: readonly FlowStep[], totalCost?: { amount: string; gas: string }): void { +export function displayFlowSummary( + steps: readonly FlowStep[], + totalCost?: { amount: string; gas: string }, +): void { const totalTime = steps.reduce((sum, s) => sum + s.timing, 0); - const paymentSteps = steps.filter((s) => s.name.toLowerCase().includes("sign") || s.name.toLowerCase().includes("retry")); + const paymentSteps = steps.filter( + (s) => s.name.toLowerCase().includes("sign") || s.name.toLowerCase().includes("retry"), + ); const paymentTime = paymentSteps.reduce((sum, s) => sum + s.timing, 0); const paymentPct = totalTime > 0 ? ((paymentTime / totalTime) * 100).toFixed(1) : "0"; diff --git a/packages/cli/src/display/receipt.ts b/packages/cli/src/display/receipt.ts index 623c8f1..d7810a1 100644 --- a/packages/cli/src/display/receipt.ts +++ b/packages/cli/src/display/receipt.ts @@ -1,5 +1,10 @@ import chalk from "chalk"; -import type { MppReceipt, ReceiptValidation, MppCredential, CredentialValidation } from "../types.js"; +import type { + MppReceipt, + ReceiptValidation, + MppCredential, + CredentialValidation, +} from "../types.js"; import { check, label, section, formatPaymentMethod } from "../utils/format.js"; import { truncateAddress } from "../utils/format.js"; @@ -7,11 +12,17 @@ export function displayReceipt(receipt: MppReceipt, validation: ReceiptValidatio const details = [ label("Challenge ID:", receipt.challengeId || chalk.dim("(none)")), label("Method:", receipt.method ? formatPaymentMethod(receipt.method) : chalk.dim("(none)")), - label("Reference:", receipt.reference ? truncateAddress(receipt.reference, 10, 6) : chalk.dim("(none)")), + label( + "Reference:", + receipt.reference ? truncateAddress(receipt.reference, 10, 6) : chalk.dim("(none)"), + ), label("Status:", receipt.status ? formatStatus(receipt.status) : chalk.dim("(none)")), label("Timestamp:", receipt.timestamp || chalk.dim("(none)")), receipt.settlement - ? label("Settlement:", `${receipt.settlement.amount} ${receipt.settlement.currency.toUpperCase()}`) + ? label( + "Settlement:", + `${receipt.settlement.amount} ${receipt.settlement.currency.toUpperCase()}`, + ) : "", ] .filter(Boolean) @@ -20,7 +31,10 @@ export function displayReceipt(receipt: MppReceipt, validation: ReceiptValidatio const checks = [ check(validation.base64Valid, "Base64 decoding valid"), check(validation.jsonValid, "JSON structure valid"), - check(validation.requiredFieldsPresent, "Required fields present (challengeId, method, reference, status, timestamp)"), + check( + validation.requiredFieldsPresent, + "Required fields present (challengeId, method, reference, status, timestamp)", + ), check(validation.timestampValid, "Timestamp not in future"), ].join("\n"); @@ -35,20 +49,36 @@ export function displayReceipt(receipt: MppReceipt, validation: ReceiptValidatio console.log(); } -export function displayCredential(credential: MppCredential, validation: CredentialValidation): void { +export function displayCredential( + credential: MppCredential, + validation: CredentialValidation, +): void { const details = [ - label("Source:", credential.source ? truncateAddress(credential.source, 10, 6) : chalk.dim("(none)")), + label( + "Source:", + credential.source ? truncateAddress(credential.source, 10, 6) : chalk.dim("(none)"), + ), "", chalk.dim(" Challenge:"), label(" ID:", credential.challenge.id || chalk.dim("(none)")), label(" Realm:", credential.challenge.realm || chalk.dim("(none)")), - label(" Method:", credential.challenge.method ? formatPaymentMethod(credential.challenge.method) : chalk.dim("(none)")), + label( + " Method:", + credential.challenge.method + ? formatPaymentMethod(credential.challenge.method) + : chalk.dim("(none)"), + ), label(" Intent:", credential.challenge.intent || chalk.dim("(none)")), - credential.challenge.request ? label(" Request:", truncateAddress(credential.challenge.request, 16, 8)) : "", + credential.challenge.request + ? label(" Request:", truncateAddress(credential.challenge.request, 16, 8)) + : "", "", chalk.dim(" Payload:"), ...Object.entries(credential.payload).map(([key, value]) => - label(` ${key}:`, typeof value === "string" ? truncateAddress(String(value), 16, 8) : JSON.stringify(value)), + label( + ` ${key}:`, + typeof value === "string" ? truncateAddress(String(value), 16, 8) : JSON.stringify(value), + ), ), ] .filter(Boolean) @@ -91,6 +121,9 @@ export function receiptToJson(receipt: MppReceipt, validation: ReceiptValidation return { receipt, validation }; } -export function credentialToJson(credential: MppCredential, validation: CredentialValidation): object { +export function credentialToJson( + credential: MppCredential, + validation: CredentialValidation, +): object { return { credential, validation }; } diff --git a/packages/cli/src/display/table.ts b/packages/cli/src/display/table.ts index d46148c..4973f4b 100644 --- a/packages/cli/src/display/table.ts +++ b/packages/cli/src/display/table.ts @@ -37,13 +37,19 @@ export function displayPriceComparison(entries: readonly CompareEntry[]): void { const valid = entries.filter((e) => !e.error); if (valid.length > 0) { - const cheapest = valid.reduce((min, e) => (parseFloat(e.price) < parseFloat(min.price) ? e : min)); + const cheapest = valid.reduce((min, e) => + parseFloat(e.price) < parseFloat(min.price) ? e : min, + ); console.log(); - console.log(` ${chalk.green("Cheapest:")} ${cheapest.service} ($${cheapest.price}/query via ${formatPaymentMethod(cheapest.paymentMethod)})`); + console.log( + ` ${chalk.green("Cheapest:")} ${cheapest.service} ($${cheapest.price}/query via ${formatPaymentMethod(cheapest.paymentMethod)})`, + ); const sessionEnabled = valid.filter((e) => e.intent === "session"); if (sessionEnabled.length > 0) { - console.log(` ${chalk.blue("Session-enabled:")} ${sessionEnabled.map((e) => e.service).join(", ")} (cheaper at volume)`); + console.log( + ` ${chalk.blue("Session-enabled:")} ${sessionEnabled.map((e) => e.service).join(", ")} (cheaper at volume)`, + ); } // Group by payment method @@ -57,7 +63,9 @@ export function displayPriceComparison(entries: readonly CompareEntry[]): void { console.log(); console.log(` ${chalk.dim("Payment methods available:")}`); for (const [method, entries] of byMethod) { - console.log(` ${formatPaymentMethod(method)}: ${entries.map((e) => e.service).join(", ")}`); + console.log( + ` ${formatPaymentMethod(method)}: ${entries.map((e) => e.service).join(", ")}`, + ); } } } @@ -110,7 +118,10 @@ export function displayBenchmarkResults(result: BenchmarkResult, url: string): v const successRate = ((result.successful / result.totalRequests) * 100).toFixed(0); const statusLine = `${result.successful}/${result.totalRequests} (${successRate}%)`; - const failLine = result.failed > 0 ? `${result.failed}/${result.totalRequests} (${result.errors.join(", ")})` : "0"; + const failLine = + result.failed > 0 + ? `${result.failed}/${result.totalRequests} (${result.errors.join(", ")})` + : "0"; const details = [ label("Successful:", chalk.green(statusLine)), diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f4f41c8..2e34322 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -49,7 +49,13 @@ export { isBlockchainMethod, resolveCurrency, } from "./utils/chains.js"; -export { truncateAddress, formatAmount, formatUsd, formatExpiry, formatPaymentMethod } from "./utils/format.js"; +export { + truncateAddress, + formatAmount, + formatUsd, + formatExpiry, + formatPaymentMethod, +} from "./utils/format.js"; export { challengeToJson } from "./display/challenge.js"; export { receiptToJson, credentialToJson } from "./display/receipt.js"; diff --git a/packages/cli/src/utils/chains.ts b/packages/cli/src/utils/chains.ts index 0ff5512..7a79609 100644 --- a/packages/cli/src/utils/chains.ts +++ b/packages/cli/src/utils/chains.ts @@ -84,10 +84,22 @@ export interface PaymentMethodInfo { const PAYMENT_METHOD_INFO: Record = { tempo: { name: "Tempo", description: "Tempo blockchain stablecoin payments", blockchain: true }, stripe: { name: "Stripe", description: "SPT-based card payments via Stripe", blockchain: false }, - lightning: { name: "Lightning", description: "Bitcoin via Lightning Network (BOLT11)", blockchain: true }, + lightning: { + name: "Lightning", + description: "Bitcoin via Lightning Network (BOLT11)", + blockchain: true, + }, solana: { name: "Solana", description: "SOL + SPL token payments", blockchain: true }, - card: { name: "Card", description: "Encrypted network tokens (Visa Intelligent Commerce)", blockchain: false }, - custom: { name: "Custom", description: "Custom payment rail via Method.from()", blockchain: false }, + card: { + name: "Card", + description: "Encrypted network tokens (Visa Intelligent Commerce)", + blockchain: false, + }, + custom: { + name: "Custom", + description: "Custom payment rail via Method.from()", + blockchain: false, + }, }; export function getPaymentMethodInfo(method: string): PaymentMethodInfo | undefined { diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts index 98cb0b2..e4d71bc 100644 --- a/packages/cli/src/utils/format.ts +++ b/packages/cli/src/utils/format.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { getTokenSymbol, getChainName, resolveCurrency } from "./chains.js"; +import { getChainName, resolveCurrency } from "./chains.js"; export function truncateAddress(addr: string, start = 6, end = 4): string { if (addr.length <= start + end + 3) return addr; @@ -76,7 +76,11 @@ export function label(key: string, value: string, keyWidth = 16): string { } export function section(title: string, content: string): string { - const border = chalk.dim("\u250C\u2500 ") + chalk.bold(title) + " " + chalk.dim("\u2500".repeat(Math.max(0, 55 - title.length))); + const border = + chalk.dim("\u250C\u2500 ") + + chalk.bold(title) + + " " + + chalk.dim("\u2500".repeat(Math.max(0, 55 - title.length))); const bottom = chalk.dim("\u2514" + "\u2500".repeat(60)); const lines = content .split("\n") diff --git a/packages/cli/src/utils/http.ts b/packages/cli/src/utils/http.ts index 09d9396..400c80b 100644 --- a/packages/cli/src/utils/http.ts +++ b/packages/cli/src/utils/http.ts @@ -57,7 +57,10 @@ export async function rawRequest(url: string, options: RequestOptions = {}): Pro } } -export async function fetchJson(url: string, timeout?: number): Promise<{ data: unknown; status: number }> { +export async function fetchJson( + url: string, + timeout?: number, +): Promise<{ data: unknown; status: number }> { const response = await rawRequest(url, { timeout }); try { const data = JSON.parse(response.body); diff --git a/packages/cli/src/utils/parser.ts b/packages/cli/src/utils/parser.ts index 67c10fe..5a4b567 100644 --- a/packages/cli/src/utils/parser.ts +++ b/packages/cli/src/utils/parser.ts @@ -11,14 +11,7 @@ import type { CredentialValidation, } from "../types.js"; -const KNOWN_CHALLENGE_FIELDS = new Set([ - "id", - "realm", - "method", - "intent", - "expires", - "request", -]); +const KNOWN_CHALLENGE_FIELDS = new Set(["id", "realm", "method", "intent", "expires", "request"]); // Legacy field names from our old format — mapped to spec equivalents const LEGACY_FIELD_MAP: Record = { @@ -94,7 +87,11 @@ function decodeRequestParam(encoded: string): MppRequestParams | null { // The real protocol nests chainId inside methodDetails for tempo // e.g. { amount, currency, recipient, methodDetails: { chainId: 42431, feePayer: true } } // Hoist chainId to top level for convenience if not already present - if (!result.chainId && typeof result.methodDetails === "object" && result.methodDetails !== null) { + if ( + !result.chainId && + typeof result.methodDetails === "object" && + result.methodDetails !== null + ) { const md = result.methodDetails as Record; if (typeof md.chainId === "number") { return { ...result, chainId: md.chainId }; @@ -146,7 +143,12 @@ export function parseChallengeHeader(raw: string): MppChallenge { const allKnownFields = new Set([ ...KNOWN_CHALLENGE_FIELDS, ...Object.keys(LEGACY_FIELD_MAP), - "amount", "currency", "recipient", "chainId", "signature", "description", + "amount", + "currency", + "recipient", + "chainId", + "signature", + "description", ]); for (const [key, value] of Object.entries(params)) { if (!allKnownFields.has(key)) { @@ -208,7 +210,10 @@ export function parseProblemDetails(body: string): MppProblemDetails | null { // --- Receipt decoder (spec-compliant) --- -export function decodeReceipt(input: string): { receipt: MppReceipt; validation: ReceiptValidation } { +export function decodeReceipt(input: string): { + receipt: MppReceipt; + validation: ReceiptValidation; +} { const errors: string[] = []; let base64Valid = false; let jsonValid = false; @@ -262,11 +267,12 @@ export function decodeReceipt(input: string): { receipt: MppReceipt; validation: const method = typeof parsed.method === "string" ? parsed.method : ""; const reference = typeof parsed.reference === "string" ? parsed.reference : ""; const status = typeof parsed.status === "string" ? parsed.status : ""; - const timestamp = typeof parsed.timestamp === "string" - ? parsed.timestamp - : typeof parsed.timestamp === "number" - ? new Date(parsed.timestamp * 1000).toISOString() - : ""; + const timestamp = + typeof parsed.timestamp === "string" + ? parsed.timestamp + : typeof parsed.timestamp === "number" + ? new Date(parsed.timestamp * 1000).toISOString() + : ""; let settlement: MppSettlement | null = null; if (typeof parsed.settlement === "object" && parsed.settlement !== null) { @@ -282,7 +288,8 @@ export function decodeReceipt(input: string): { receipt: MppReceipt; validation: const hasLegacyFields = "receiptId" in parsed || "credential" in parsed; const hasSpecFields = !!reference && !!status; if (hasLegacyFields && !hasSpecFields) { - const legacyChallengeId = challengeId || (typeof parsed.receiptId === "string" ? parsed.receiptId : ""); + const legacyChallengeId = + challengeId || (typeof parsed.receiptId === "string" ? parsed.receiptId : ""); const legacyAmount = typeof parsed.amount === "string" ? parsed.amount : ""; const legacyCredential = typeof parsed.credential === "string" ? parsed.credential : ""; @@ -297,7 +304,17 @@ export function decodeReceipt(input: string): { receipt: MppReceipt; validation: if (!timestampValid) errors.push("Timestamp is in the future"); } - const knownKeys = new Set(["receiptId", "timestamp", "credential", "challengeId", "amount", "method", "reference", "settlement", "status"]); + const knownKeys = new Set([ + "receiptId", + "timestamp", + "credential", + "challengeId", + "amount", + "method", + "reference", + "settlement", + "status", + ]); const extra: Record = {}; for (const [key, value] of Object.entries(parsed)) { if (!knownKeys.has(key)) extra[key] = value; @@ -333,7 +350,14 @@ export function decodeReceipt(input: string): { receipt: MppReceipt; validation: if (isNaN(ts)) errors.push("Invalid timestamp format"); } - const knownKeys = new Set(["challengeId", "method", "reference", "settlement", "status", "timestamp"]); + const knownKeys = new Set([ + "challengeId", + "method", + "reference", + "settlement", + "status", + "timestamp", + ]); const extra: Record = {}; for (const [key, value] of Object.entries(parsed)) { if (!knownKeys.has(key)) extra[key] = value; @@ -347,7 +371,10 @@ export function decodeReceipt(input: string): { receipt: MppReceipt; validation: // --- Credential decoder --- -export function decodeCredential(input: string): { credential: MppCredential; validation: CredentialValidation } { +export function decodeCredential(input: string): { + credential: MppCredential; + validation: CredentialValidation; +} { const errors: string[] = []; let base64Valid = false; let jsonValid = false; @@ -375,7 +402,15 @@ export function decodeCredential(input: string): { credential: MppCredential; va errors.push("Invalid base64 encoding"); return { credential: emptyCredential, - validation: { base64Valid, jsonValid, structureValid, challengePresent, sourcePresent, payloadPresent, errors }, + validation: { + base64Valid, + jsonValid, + structureValid, + challengePresent, + sourcePresent, + payloadPresent, + errors, + }, }; } } @@ -396,7 +431,15 @@ export function decodeCredential(input: string): { credential: MppCredential; va errors.push("Invalid JSON structure"); return { credential: emptyCredential, - validation: { base64Valid, jsonValid, structureValid, challengePresent, sourcePresent, payloadPresent, errors }, + validation: { + base64Valid, + jsonValid, + structureValid, + challengePresent, + sourcePresent, + payloadPresent, + errors, + }, }; } } @@ -422,9 +465,10 @@ export function decodeCredential(input: string): { credential: MppCredential; va request: typeof ch.request === "string" ? ch.request : "", }; - const payload = typeof parsed.payload === "object" && parsed.payload !== null - ? (parsed.payload as Record) - : {}; + const payload = + typeof parsed.payload === "object" && parsed.payload !== null + ? (parsed.payload as Record) + : {}; return { credential: { @@ -433,7 +477,15 @@ export function decodeCredential(input: string): { credential: MppCredential; va payload, raw: input, }, - validation: { base64Valid, jsonValid, structureValid, challengePresent, sourcePresent, payloadPresent, errors }, + validation: { + base64Valid, + jsonValid, + structureValid, + challengePresent, + sourcePresent, + payloadPresent, + errors, + }, }; } @@ -452,7 +504,12 @@ export function parseMppManifest(json: unknown): MppManifest { .map((e) => ({ method: typeof e.method === "string" ? e.method : "GET", path: typeof e.path === "string" ? e.path : String(e.path ?? ""), - price: typeof e.price === "string" ? e.price : typeof e.price === "number" ? String(e.price) : undefined, + price: + typeof e.price === "string" + ? e.price + : typeof e.price === "number" + ? String(e.price) + : undefined, description: typeof e.description === "string" ? e.description : undefined, intent: typeof e.intent === "string" ? e.intent : undefined, currency: typeof e.currency === "string" ? e.currency : undefined, diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index a8ae390..5979119 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -3,6 +3,19 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["src/__tests__/**/*.test.ts"], + exclude: ["src/__tests__/integration/**"], pool: "forks", + coverage: { + provider: "v8", + reporter: ["text", "lcov", "json-summary"], + include: ["src/**/*.ts"], + exclude: ["src/__tests__/**", "src/**/index.ts", "src/commands/**", "src/display/**"], + thresholds: { + statements: 80, + branches: 75, + functions: 75, + lines: 80, + }, + }, }, }); diff --git a/packages/cli/vitest.integration.config.ts b/packages/cli/vitest.integration.config.ts new file mode 100644 index 0000000..2b6eff5 --- /dev/null +++ b/packages/cli/vitest.integration.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/__tests__/integration/**/*.test.ts"], + pool: "forks", + testTimeout: 30_000, + }, +}); diff --git a/packages/mock-server/package.json b/packages/mock-server/package.json index 7f10b1a..a794ef5 100644 --- a/packages/mock-server/package.json +++ b/packages/mock-server/package.json @@ -6,8 +6,14 @@ "bin": { "mpp-mock-server": "./dist/bin/mpp-mock-server.js" }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "exports": { - ".": "./dist/index.js" + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } }, "files": [ "dist" @@ -33,7 +39,7 @@ "directory": "packages/mock-server" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/packages/mock-server/src/index.ts b/packages/mock-server/src/index.ts index b28678e..b2b1f6b 100644 --- a/packages/mock-server/src/index.ts +++ b/packages/mock-server/src/index.ts @@ -1,2 +1,7 @@ export { createMockServer } from "./server.js"; -export { DEMO_ENDPOINTS, DEMO_MANIFEST, buildDemoReceipt, buildDemoCredential } from "./fixtures.js"; +export { + DEMO_ENDPOINTS, + DEMO_MANIFEST, + buildDemoReceipt, + buildDemoCredential, +} from "./fixtures.js"; diff --git a/packages/mock-server/src/server.ts b/packages/mock-server/src/server.ts index 652f96b..2e32c1b 100644 --- a/packages/mock-server/src/server.ts +++ b/packages/mock-server/src/server.ts @@ -1,5 +1,11 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; -import { DEMO_ENDPOINTS, DEMO_MANIFEST, LLMS_TXT, buildChallengeHeader, buildDemoReceipt } from "./fixtures.js"; +import { + DEMO_ENDPOINTS, + DEMO_MANIFEST, + LLMS_TXT, + buildChallengeHeader, + buildDemoReceipt, +} from "./fixtures.js"; export interface MockServerOptions { port?: number; @@ -67,7 +73,9 @@ function handleRequest(req: IncomingMessage, res: ServerResponse, silent: boolea if (path === "/v1/validate-receipt") { let body = ""; - req.on("data", (chunk: Buffer) => { body += chunk.toString(); }); + req.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); req.on("end", () => { log(silent, ` 200 receipt accepted`); json(res, 200, { valid: true, received: body.length > 0 }); @@ -82,7 +90,11 @@ function handleRequest(req: IncomingMessage, res: ServerResponse, silent: boolea log(silent, ` 200 OK (authorized)`); // Generate a spec-compliant receipt - const receiptBase64 = buildDemoReceipt("demo-challenge", endpoint.paymentMethod, endpoint.price); + const receiptBase64 = buildDemoReceipt( + "demo-challenge", + endpoint.paymentMethod, + endpoint.price, + ); json(res, 200, { data: { message: `Authorized access to ${endpoint.description}`, endpoint: path }, @@ -99,12 +111,14 @@ function handleRequest(req: IncomingMessage, res: ServerResponse, silent: boolea "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }); - res.end(JSON.stringify({ - error: "Payment Required", - message: `This endpoint requires payment of ${endpoint.price} ${endpoint.currency} via ${endpoint.paymentMethod}`, - protocol: "MPP", - docs: "https://mpp.dev/overview", - })); + res.end( + JSON.stringify({ + error: "Payment Required", + message: `This endpoint requires payment of ${endpoint.price} ${endpoint.currency} via ${endpoint.paymentMethod}`, + protocol: "MPP", + docs: "https://mpp.dev/overview", + }), + ); return; } @@ -129,7 +143,9 @@ export function createMockServer(options: MockServerOptions = {}) { console.log(); console.log(` Endpoints:`); for (const ep of DEMO_ENDPOINTS) { - console.log(` ${ep.httpMethod.padEnd(5)} ${ep.path.padEnd(20)} ${ep.price.padEnd(8)} ${ep.currency} (${ep.paymentMethod}, ${ep.intent})`); + console.log( + ` ${ep.httpMethod.padEnd(5)} ${ep.path.padEnd(20)} ${ep.price.padEnd(8)} ${ep.currency} (${ep.paymentMethod}, ${ep.intent})`, + ); } console.log(); console.log(` Discovery:`); diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 09f5f92..86eafd8 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -38,7 +38,7 @@ "directory": "packages/plugin" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/packages/plugin/src/hooks/session-check.ts b/packages/plugin/src/hooks/session-check.ts index 5638afe..7c3b74d 100644 --- a/packages/plugin/src/hooks/session-check.ts +++ b/packages/plugin/src/hooks/session-check.ts @@ -29,11 +29,7 @@ function detectMpp(): boolean { if (existsSync(join(cwd, ".well-known", "mpp.json"))) return true; if (existsSync(join(cwd, "public", ".well-known", "mpp.json"))) return true; - const filesToCheck = [ - "package.json", - "README.md", - "llms.txt", - ]; + const filesToCheck = ["package.json", "README.md", "llms.txt"]; for (const file of filesToCheck) { if (checkFile(join(cwd, file))) return true; diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index ffb3af9..c47b4b2 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -40,7 +40,10 @@ server.tool( "Discover MPP endpoints on a domain by checking /.well-known/mpp.json, /llms.txt, /health, and optionally probing common API paths.", { domain: z.string().describe("Domain to scan (e.g. findata.example.com)"), - probe: z.boolean().default(false).describe("Also probe common API paths with HEAD requests to find 402 endpoints"), + probe: z + .boolean() + .default(false) + .describe("Also probe common API paths with HEAD requests to find 402 endpoints"), timeout: z.number().default(10000).describe("Request timeout in milliseconds"), }, async (input) => { diff --git a/packages/plugin/src/tools/compare.ts b/packages/plugin/src/tools/compare.ts index ef764dd..167267a 100644 --- a/packages/plugin/src/tools/compare.ts +++ b/packages/plugin/src/tools/compare.ts @@ -82,8 +82,6 @@ export async function comparePricing(input: CompareInput): Promise { return { ...compareToJson(results), - summary: cheapest - ? { cheapest: cheapest.service, price: cheapest.price } - : { cheapest: null }, + summary: cheapest ? { cheapest: cheapest.service, price: cheapest.price } : { cheapest: null }, }; } diff --git a/packages/plugin/src/tools/scan.ts b/packages/plugin/src/tools/scan.ts index eac6f18..6675f7d 100644 --- a/packages/plugin/src/tools/scan.ts +++ b/packages/plugin/src/tools/scan.ts @@ -37,7 +37,8 @@ export async function scanDomain(input: ScanInput): Promise { try { const response = await rawRequest(`${baseUrl}/llms.txt`, { timeout }); - discoveries["/llms.txt"] = response.status === 200 && response.body.length > 0 ? "found" : "not found"; + discoveries["/llms.txt"] = + response.status === 200 && response.body.length > 0 ? "found" : "not found"; } catch { discoveries["/llms.txt"] = "error"; }