From 15b00f008e25296e0dd9274e41026d6e24453335 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Mon, 12 Oct 2026 20:42:05 +0500 Subject: [PATCH 01/11] feat: enforce strict MCP quality gates and deterministic generated contracts --- .dependency-cruiser.cjs | 58 + .github/workflows/quality.yml | 53 + .gitignore | 7 + .husky/pre-commit | 1 + .nvmrc | 1 + .prettierignore | 9 + .prettierrc.json | 5 + AGENTS.md | 13 + ARCHITECTURE.md | 8 +- Dockerfile | 4 +- README.md | 45 +- eslint.complexity.config.mjs | 21 + eslint.config.mjs | 27 + eslint.typed.config.mjs | 14 + knip.json | 14 + package-lock.json | 23 +- package.json | 28 +- quality.config.mjs | 47 + quality/baselines/complexity.json | 62 + quality/baselines/duplicates.json | 38 + quality/baselines/unused-full.json | 34 + quality/baselines/unused-production.json | 76 + quality/mcp/generate.mjs | 149 + quality/mcp/inventory.json | 4 + quality/mcp/openapi.yaml | 4621 +++++++++++ quality/mcp/provenance.json | 22 + quality/mcp/sdk-compatibility-exception.json | 141 + quality/mcp/vendor/apis/CallingApi.ts | 328 + quality/mcp/vendor/apis/CatalogApi.ts | 228 + quality/mcp/vendor/apis/ChannelApi.ts | 492 ++ quality/mcp/vendor/apis/FlowsApi.ts | 1177 +++ quality/mcp/vendor/apis/GroupsApi.ts | 719 ++ quality/mcp/vendor/apis/MessagingApi.ts | 2716 +++++++ quality/mcp/vendor/apis/ProfileApi.ts | 228 + quality/mcp/vendor/apis/TemplatesApi.ts | 445 ++ quality/mcp/vendor/apis/UsersApi.ts | 317 + quality/mcp/vendor/apis/WebhooksApi.ts | 228 + quality/mcp/vendor/generate-mcp-tools.cjs | 900 +++ quality/mcp/verify-negative.mjs | 31 + scripts/pack-smoke.mjs | 54 + scripts/quality-negative.mjs | 58 + scripts/smoke-tools-list.js | 12 +- src/__tests__/auth.test.ts | 16 +- src/__tests__/config.test.ts | 8 +- src/__tests__/handlers.test.ts | 15 +- src/__tests__/http.test.ts | 1 + src/__tests__/json-schema-to-zod.test.ts | 8 +- src/__tests__/mock-sdk.ts | 12 +- .../polyfill-channel-settings.test.ts | 16 +- src/__tests__/rate-limit.test.ts | 4 +- src/__tests__/sdk-only-architecture.test.ts | 11 +- src/__tests__/server.test.ts | 17 +- src/__tests__/tools-call.test.ts | 28 +- src/auth.ts | 22 +- src/config.ts | 15 +- src/http.ts | 70 +- src/json-schema-to-zod.ts | 20 +- src/logging.ts | 2 +- src/polyfill-channel-settings.ts | 43 +- src/server.ts | 26 +- src/tools.generated.ts | 68 +- tools/quality/README.md | 85 + tools/quality/eslint.mjs | 137 + tools/quality/lib/baseline.mjs | 82 + tools/quality/lib/git.mjs | 86 + tools/quality/lib/guards.mjs | 356 + tools/quality/lib/historical.mjs | 139 + tools/quality/lib/knip-reporter.mjs | 42 + tools/quality/lib/reports.mjs | 265 + tools/quality/lib/template.mjs | 56 + tools/quality/package-lock.json | 6852 +++++++++++++++++ tools/quality/package.json | 34 + tools/quality/prettier.config.mjs | 2 + tools/quality/run.mjs | 360 + tools/quality/seal.mjs | 19 + tools/quality/template-manifest.json | 25 + tools/quality/test/cli.test.mjs | 510 ++ tools/quality/test/ratchet.test.mjs | 238 + tools/quality/tsconfig.next.json | 16 + tools/quality/tsconfig.node.json | 13 + tools/quality/tsconfig.svelte.json | 14 + tsconfig.json | 9 +- tsconfig.quality.json | 19 + 83 files changed, 22975 insertions(+), 244 deletions(-) create mode 100644 .dependency-cruiser.cjs create mode 100644 .github/workflows/quality.yml create mode 100644 .husky/pre-commit create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 AGENTS.md create mode 100644 eslint.complexity.config.mjs create mode 100644 eslint.config.mjs create mode 100644 eslint.typed.config.mjs create mode 100644 knip.json create mode 100644 quality.config.mjs create mode 100644 quality/baselines/complexity.json create mode 100644 quality/baselines/duplicates.json create mode 100644 quality/baselines/unused-full.json create mode 100644 quality/baselines/unused-production.json create mode 100644 quality/mcp/generate.mjs create mode 100644 quality/mcp/inventory.json create mode 100644 quality/mcp/openapi.yaml create mode 100644 quality/mcp/provenance.json create mode 100644 quality/mcp/sdk-compatibility-exception.json create mode 100644 quality/mcp/vendor/apis/CallingApi.ts create mode 100644 quality/mcp/vendor/apis/CatalogApi.ts create mode 100644 quality/mcp/vendor/apis/ChannelApi.ts create mode 100644 quality/mcp/vendor/apis/FlowsApi.ts create mode 100644 quality/mcp/vendor/apis/GroupsApi.ts create mode 100644 quality/mcp/vendor/apis/MessagingApi.ts create mode 100644 quality/mcp/vendor/apis/ProfileApi.ts create mode 100644 quality/mcp/vendor/apis/TemplatesApi.ts create mode 100644 quality/mcp/vendor/apis/UsersApi.ts create mode 100644 quality/mcp/vendor/apis/WebhooksApi.ts create mode 100644 quality/mcp/vendor/generate-mcp-tools.cjs create mode 100644 quality/mcp/verify-negative.mjs create mode 100644 scripts/pack-smoke.mjs create mode 100644 scripts/quality-negative.mjs create mode 100644 tools/quality/README.md create mode 100644 tools/quality/eslint.mjs create mode 100644 tools/quality/lib/baseline.mjs create mode 100644 tools/quality/lib/git.mjs create mode 100644 tools/quality/lib/guards.mjs create mode 100644 tools/quality/lib/historical.mjs create mode 100644 tools/quality/lib/knip-reporter.mjs create mode 100644 tools/quality/lib/reports.mjs create mode 100644 tools/quality/lib/template.mjs create mode 100644 tools/quality/package-lock.json create mode 100644 tools/quality/package.json create mode 100644 tools/quality/prettier.config.mjs create mode 100644 tools/quality/run.mjs create mode 100644 tools/quality/seal.mjs create mode 100644 tools/quality/template-manifest.json create mode 100644 tools/quality/test/cli.test.mjs create mode 100644 tools/quality/test/ratchet.test.mjs create mode 100644 tools/quality/tsconfig.next.json create mode 100644 tools/quality/tsconfig.node.json create mode 100644 tools/quality/tsconfig.svelte.json create mode 100644 tsconfig.quality.json diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs new file mode 100644 index 0000000..c0b6d15 --- /dev/null +++ b/.dependency-cruiser.cjs @@ -0,0 +1,58 @@ +// Owners and constraints for this repository's actual runtime roots. +module.exports = { + forbidden: [ + { + name: 'no-runtime-cycles', + severity: 'error', + from: {}, + to: { + circular: true, + dependencyTypesNot: ['type-only'], + }, + }, + { + name: 'no-unresolved-dependencies', + severity: 'error', + from: {}, + to: { + couldNotResolve: true, + }, + }, + { + name: 'no-runtime-test-dependencies', + severity: 'error', + from: { + path: '^(?:src)/', + pathNot: '(?:__tests__|test|tests)/|\\.(?:test|spec)\\.', + }, + to: { + path: '(?:^|/)(?:__tests__|tests|test)/|(?:^|node_modules/)(?:jest|vitest|ts-jest)', + }, + }, + { + name: 'no-sdk-in-pure-transforms', + severity: 'error', + from: { + path: '^src/(?:auth|json-schema-to-zod|logging|rate-limit)\\.ts$', + }, + to: { + path: 'node_modules/(?:@1msg|express|@modelcontextprotocol)/', + }, + }, + ], + options: { + doNotFollow: { + path: 'node_modules', + }, + tsConfig: { + fileName: 'tsconfig.quality.json', + }, + tsPreCompilationDeps: true, + enhancedResolveOptions: { + exportsFields: ['exports'], + conditionNames: ['require', 'node', 'default'], + extensions: ['.js', '.ts', '.json'], + }, + exclude: '(?:^|/)node_modules/', + }, +}; diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..7929766 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,53 @@ +name: Quality +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +concurrency: + group: quality-${{ github.ref }} + cancel-in-progress: true +jobs: + checks: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '24.19.0' + - run: npm ci + - run: npm --prefix tools/quality ci + - run: npm run test:preset + - run: npm run quality:guards + - run: npm run format:check + - run: npm run lint:static + - run: npm run lint:typed + - run: npm run typecheck + - run: npm run lint:arch + - run: npm run lint:unused:production + - run: npm run lint:unused:full + - run: npm run lint:duplicates + - run: npm run lint:duplicates:full + - run: npm run lint:complexity + - run: npm run test:generated + - run: npm run test:quality + - run: npm test + - run: npm run build + - run: npm run test:pack + - uses: actions/upload-artifact@v4 + if: always() + with: + name: quality-reports + path: quality/reports/ + if-no-files-found: error + quality-required: + if: always() + needs: [checks] + runs-on: ubuntu-24.04 + steps: + - env: + RESULTS: ${{ toJSON(needs) }} + run: node -e 'const jobs=JSON.parse(process.env.RESULTS); if(!Object.values(jobs).every(job=>job.result==="success")) process.exit(1)' diff --git a/.gitignore b/.gitignore index 59596ef..97d2005 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ coverage/ !.env.example npm-debug.log* node_modules/ + +quality/reports/ + +.husky/_/ + +# Local shared-tool dependency links are never repository content. +tools/quality/node_modules diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..81e401a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +node tools/quality/node_modules/lint-staged/bin/lint-staged.js diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..60ade1a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.19.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..db71125 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +node_modules +dist +tools/quality +quality/reports +src/tools.generated.ts +src/handlers.generated.ts + +quality/mcp/vendor/ +quality/mcp/openapi.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..2fec1f2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6e5e0b0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# Quality and ownership + +Profile A, Node 24.19.0. SDK/tooling maintainers own this repository. The public package entrypoints are defined in package.json; framework registration and executable consumers must remain intact. Imports must be acyclic at runtime, production cannot import tests, and dependency boundaries are enforced in `.dependency-cruiser.cjs`. `quality.config.mjs` is the source inventory and exact generated-path declaration. + +Configuration belongs in the existing config/bootstrap adapters; pure conversion/validation helpers must not gain transport or environment reads. SDK and HTTP calls belong in the existing transport/client adapters. Keep complete scenarios readable at their application entrypoints; avoid pass-through facade layers. Comments on new/changed functions describe purpose, inputs/results, invariants and relevant failure/resource ownership. Check comments against behavior rather than repeating the function name. + +Install with the frozen lockfile, then `npm --prefix tools/quality ci`. Run `npm run hooks:install` for local Husky/lint-staged feedback. Run all commands listed in `.github/workflows/quality.yml` before review: formatter, static and typed ESLint, strict production/test typecheck, dependency-cruiser, both Knip graphs, jscpd runtime gate and full report, complexity, guard negative fixtures, tests, build and packed-entrypoint smoke. No CI skip variable disables typed lint. + +Historical debt is recorded per finding in `quality/baselines`, with adoption SHA, evidence, owner, issue and expiry. Initial allowances are replayed against the default branch; new findings and increases are forbidden. Remove entries when fixed. Generated paths have producer provenance; authored tooling is always checked. + +The `quality-required` job fails on failed/skipped/cancelled prerequisites. Configure branch protection to require it; YAML alone does not change server protection. Tests use isolated/local inputs; never point them at production services. + +Generated MCP files are owned by the pinned API producer. Run `npm run test:generated` to replay the exact producer against the checksummed public contract and locked SDK declarations, including changed/deleted/ignored-output negative fixtures. Update the producer provenance before deliberately running `node quality/mcp/generate.mjs --write`; never hand-edit the two generated files. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2ba5a72..e00cdd5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -13,7 +13,7 @@ OpenAPI YAML (1msg-api monorepo) Generated files (`src/tools.generated.ts`, `src/handlers.generated.ts`) are produced in `1msg-api` and synced into this distribution repo when tools change. -| Transport | Entry | Config | -|-----------|-------|--------| -| stdio | `dist/index.js` | Process env `ONE_MSG_*` (deprecated: `CHAT_API_*`). API root: `https://api.1msg.io` or `https://sandbox.1msg.io` | -| HTTP (Cloud) | `dist/http.js` | Per request: `Authorization: Bearer` + `X-Instance-Id`; optional `X-1msg-Base-Url` (`https://api.1msg.io` or `https://sandbox.1msg.io`). Default upstream from host `ONE_MSG_BASE_URL` | +| Transport | Entry | Config | +| ------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| stdio | `dist/index.js` | Process env `ONE_MSG_*` (deprecated: `CHAT_API_*`). API root: `https://api.1msg.io` or `https://sandbox.1msg.io` | +| HTTP (Cloud) | `dist/http.js` | Per request: `Authorization: Bearer` + `X-Instance-Id`; optional `X-1msg-Base-Url` (`https://api.1msg.io` or `https://sandbox.1msg.io`). Default upstream from host `ONE_MSG_BASE_URL` | diff --git a/Dockerfile b/Dockerfile index 5663d79..e640828 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # docker build -t 1msg/1msg-mcp-http:local . # docker run --rm -p 3100:3100 -e ONE_MSG_BASE_URL=https://api.1msg.io 1msg/1msg-mcp-http:local -FROM node:20-alpine AS build +FROM node:24.19.0-alpine AS build WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci || npm install @@ -10,7 +10,7 @@ COPY tsconfig.json tsconfig.build.json ./ COPY src ./src RUN npm run build && npm prune --omit=dev -FROM node:20-alpine +FROM node:24.19.0-alpine WORKDIR /app ENV NODE_ENV=production ENV MCP_HTTP_HOST=0.0.0.0 diff --git a/README.md b/README.md index 0828033..765684e 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ MCP server for the [1MSG](https://1msg.io) WhatsApp Business API — **62 public Transports: **stdio** (local) and **Streamable HTTP** (Cloud). -| | | -|--|--| -| **npm** | [`@1msg/mcp`](https://www.npmjs.com/package/@1msg/mcp) | -| **Cloud** | https://mcp.1msg.io/mcp | -| **SDK** | [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk) | +| | | +| --------- | ------------------------------------------------------ | +| **npm** | [`@1msg/mcp`](https://www.npmjs.com/package/@1msg/mcp) | +| **Cloud** | https://mcp.1msg.io/mcp | +| **SDK** | [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk) | API hosts: -| Environment | `ONE_MSG_BASE_URL` | -|-------------|--------------------| -| Live channels | `https://api.1msg.io` | +| Environment | `ONE_MSG_BASE_URL` | +| ------------- | ------------------------- | +| Live channels | `https://api.1msg.io` | | Test channels | `https://sandbox.1msg.io` | > Source of truth for **codegen** of `tools.generated.ts` / `handlers.generated.ts` still lives in the `1msg-api` monorepo (`packages/mcp`). This repository is the **distribution** package for running and hosting the server. @@ -35,7 +35,6 @@ npx -y @1msg/mcp For **test channels**, set `ONE_MSG_BASE_URL=https://sandbox.1msg.io` (bare `sandbox.1msg.io` is also accepted). - ### Clone and run ```bash @@ -54,14 +53,14 @@ npm run start:http # Streamable HTTP → dist/http.js ## Environment variables -| Variable | Required | Description | -|----------|----------|-------------| -| `ONE_MSG_BASE_URL` | yes (stdio) | API root: `https://api.1msg.io` (live) or `https://sandbox.1msg.io` (test) | -| `ONE_MSG_TOKEN` | yes (stdio) | Channel API token | -| `ONE_MSG_INSTANCE_ID` | yes (stdio) | Channel instance id | -| `MCP_HTTP_HOST` | no | HTTP bind host (default `0.0.0.0` in Docker) | -| `MCP_HTTP_PORT` | no | HTTP port (default `3100`) | -| `MCP_RATE_LIMIT_RPM` | no | Per-token rate limit for HTTP (default `60`) | +| Variable | Required | Description | +| --------------------- | ----------- | -------------------------------------------------------------------------- | +| `ONE_MSG_BASE_URL` | yes (stdio) | API root: `https://api.1msg.io` (live) or `https://sandbox.1msg.io` (test) | +| `ONE_MSG_TOKEN` | yes (stdio) | Channel API token | +| `ONE_MSG_INSTANCE_ID` | yes (stdio) | Channel instance id | +| `MCP_HTTP_HOST` | no | HTTP bind host (default `0.0.0.0` in Docker) | +| `MCP_HTTP_PORT` | no | HTTP port (default `3100`) | +| `MCP_RATE_LIMIT_RPM` | no | Per-token rate limit for HTTP (default `60`) | For **Cloud HTTP**, the server default upstream is `ONE_MSG_BASE_URL` (typically live). Clients send credentials per request; test channels can also send `X-1msg-Base-Url: https://sandbox.1msg.io`. @@ -132,12 +131,12 @@ docker run --rm -p 3100:3100 \ ## Scripts -| Script | Purpose | -|--------|---------| -| `npm run build` | Compile TypeScript → `dist/` | -| `npm test` | Jest unit tests | -| `npm run smoke:tools-list` | Stdio `tools/list` smoke (expects 62 tools; uses dummy token) | -| `npm start` / `npm run start:http` | Run stdio / HTTP servers | +| Script | Purpose | +| ---------------------------------- | ------------------------------------------------------------- | +| `npm run build` | Compile TypeScript → `dist/` | +| `npm test` | Jest unit tests | +| `npm run smoke:tools-list` | Stdio `tools/list` smoke (expects 62 tools; uses dummy token) | +| `npm start` / `npm run start:http` | Run stdio / HTTP servers | ## Architecture diff --git a/eslint.complexity.config.mjs b/eslint.complexity.config.mjs new file mode 100644 index 0000000..2b03820 --- /dev/null +++ b/eslint.complexity.config.mjs @@ -0,0 +1,21 @@ +import { maintainabilityConfig, tseslint } from './tools/quality/eslint.mjs'; +export default [ + { linterOptions: { reportUnusedDisableDirectives: 'off' } }, + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/tools/quality/**', + 'quality/mcp/vendor/**', + '**/quality/reports/**', + 'src/tools.generated.ts', + 'src/handlers.generated.ts', + ], + }, + { + files: ['**/*.{ts,tsx,cts,mts}'], + languageOptions: { parser: tseslint.parser }, + plugins: { '@typescript-eslint': tseslint.plugin }, + }, + ...maintainabilityConfig(), +]; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..23adb8f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,27 @@ +import { staticConfig, globals } from './tools/quality/eslint.mjs'; + +export default [ + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/tools/quality/**', + 'quality/mcp/vendor/**', + '**/quality/reports/**', + 'src/tools.generated.ts', + 'src/handlers.generated.ts', + ], + }, + ...staticConfig({ globals: { ...globals.node, ...globals.jest } }), + { + settings: { + 'import/resolver': { + typescript: { + project: './tsconfig.quality.json', + conditionNames: ['require', 'node', 'default'], + }, + node: { extensions: ['.js', '.json', '.ts', '.mjs', '.cjs'] }, + }, + }, + }, +]; diff --git a/eslint.typed.config.mjs b/eslint.typed.config.mjs new file mode 100644 index 0000000..19b53ed --- /dev/null +++ b/eslint.typed.config.mjs @@ -0,0 +1,14 @@ +import { typedConfig } from './tools/quality/eslint.mjs'; +export default [ + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/tools/quality/**', + '**/quality/reports/**', + 'src/tools.generated.ts', + 'src/handlers.generated.ts', + ], + }, + ...typedConfig({ project: './tsconfig.quality.json', tsconfigRootDir: import.meta.dirname }), +]; diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..f87ea47 --- /dev/null +++ b/knip.json @@ -0,0 +1,14 @@ +{ + "entry": [ + "src/index.ts!", + "src/http.ts!", + "scripts/*.{js,mjs,cjs}", + "jest.config.js", + "eslint*.mjs", + "quality.config.mjs", + "quality/mcp/generate.mjs!", + "quality/mcp/verify-negative.mjs!" + ], + "project": ["src/**/*.ts!", "scripts/*.{js,mjs,cjs}", "quality/mcp/*.mjs!"], + "ignore": ["tools/quality/**"] +} diff --git a/package-lock.json b/package-lock.json index d2687e2..bbf4d8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.2.8", "license": "MIT", "dependencies": { - "@1msg/sdk": "1.0.3", + "@1msg/sdk": "^1.0.3", "@modelcontextprotocol/sdk": "^1.29.0", "express": "^5.2.1", "zod": "^4.4.3" @@ -25,10 +25,11 @@ "@types/node": "^22.20.1", "jest": "^29.5.0", "ts-jest": "^29.1.0", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "yaml": "2.9.0" }, "engines": { - "node": ">=18.0.0" + "node": "24.x" } }, "node_modules/@1msg/sdk": { @@ -4919,6 +4920,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", diff --git a/package.json b/package.json index bc12043..b9136b4 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,24 @@ "start:http": "node dist/http.js", "test": "jest", "smoke:tools-list": "node scripts/smoke-tools-list.js", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "typecheck": "tsc -p tsconfig.quality.json", + "lint:static": "node tools/quality/node_modules/eslint/bin/eslint.js . --max-warnings=0", + "lint:typed": "node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.typed.config.mjs src --max-warnings=0", + "lint:unused:production": "node tools/quality/run.mjs unused-production", + "lint:unused:full": "node tools/quality/run.mjs unused-full", + "lint:duplicates": "node tools/quality/run.mjs duplicates", + "quality:guards": "node tools/quality/run.mjs guards", + "lint:arch": "node tools/quality/node_modules/dependency-cruiser/bin/dependency-cruiser.mjs --config .dependency-cruiser.cjs src", + "test:quality": "node scripts/quality-negative.mjs src", + "lint:complexity": "node tools/quality/run.mjs report --kind eslint --name complexity -- node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.complexity.config.mjs . --format json", + "lint:duplicates:full": "node tools/quality/run.mjs duplicates-full", + "format:check": "node tools/quality/node_modules/prettier/bin/prettier.cjs --check .", + "format": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write .", + "test:pack": "node scripts/pack-smoke.mjs", + "test:preset": "node --test tools/quality/test/*.test.mjs", + "hooks:install": "node tools/quality/node_modules/husky/bin.js", + "test:generated": "node quality/mcp/generate.mjs && node quality/mcp/verify-negative.mjs" }, "dependencies": { "@1msg/sdk": "^1.0.3", @@ -48,9 +65,14 @@ "@types/node": "^22.20.1", "jest": "^29.5.0", "ts-jest": "^29.1.0", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "yaml": "2.9.0" }, "engines": { - "node": ">=18.0.0" + "node": "24.x" + }, + "packageManager": "npm@11.17.0", + "lint-staged": { + "*.{js,ts,mjs,cjs,json,md,yml,yaml}": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write" } } diff --git a/quality.config.mjs b/quality.config.mjs new file mode 100644 index 0000000..5af5c7f --- /dev/null +++ b/quality.config.mjs @@ -0,0 +1,47 @@ +export default { + schemaVersion: 1, + profile: 'A', + defaultBranch: 'main', + sourceRoots: [ + 'src', + 'quality/mcp', + 'scripts', + 'jest.config.js', + 'eslint.config.mjs', + 'eslint.typed.config.mjs', + 'eslint.complexity.config.mjs', + '.dependency-cruiser.cjs', + 'quality.config.mjs', + ], + generated: [ + { + pattern: 'quality/mcp/vendor/**', + producer: + 'Pinned 1msg-api gen/generate-mcp-tools.js; checksum verified by quality/mcp/generate.mjs', + }, + { + pattern: 'src/tools.generated.ts', + producer: 'node quality/mcp/generate.mjs --write', + }, + { + pattern: 'src/handlers.generated.ts', + producer: 'node quality/mcp/generate.mjs --write', + }, + ], + comments: { + full: ['scripts/pack-smoke.mjs', 'scripts/quality-negative.mjs'], + }, + knipConfig: 'knip.json', + duplicatePaths: ['src', 'scripts'], + duplicateIgnore: [ + 'src/tools.generated.ts', + 'src/handlers.generated.ts', + '**/__tests__/**', + '**/*.test.*', + '**/*.spec.*', + ], + baselineDir: 'quality/baselines', + duplicateMaxPercentage: 5, + duplicateFullPaths: ['src', 'scripts'], + duplicateFullIgnore: ['src/tools.generated.ts', 'src/handlers.generated.ts'], +}; diff --git a/quality/baselines/complexity.json b/quality/baselines/complexity.json new file mode 100644 index 0000000..fd67bc4 --- /dev/null +++ b/quality/baselines/complexity.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": 1, + "adoptionBase": "b46574a4563dc7cae6a7dfc5b378058c01053aa8", + "entries": [ + { + "key": "src/http.ts|max-lines-per-function|createHttpApp|0", + "value": 176, + "path": "src/http.ts", + "rule": "max-lines-per-function", + "scope": "createHttpApp", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/http.ts": "cff07d8244daba9dae1551c4844f15a4c923bbd3f33ab3cd7a5f67987567a6e8" + } + }, + { + "key": "src/http.ts|max-lines-per-function|handleMcp|0", + "value": 144, + "path": "src/http.ts", + "rule": "max-lines-per-function", + "scope": "handleMcp", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/http.ts": "cff07d8244daba9dae1551c4844f15a4c923bbd3f33ab3cd7a5f67987567a6e8" + } + }, + { + "key": "src/polyfill-channel-settings.ts|max-params||0", + "value": 6, + "path": "src/polyfill-channel-settings.ts", + "rule": "max-params", + "scope": "", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/polyfill-channel-settings.ts": "0f6513f22ca02aa9809be547ee83daea2508c082a7118a34df4da5c3b8d39fdc" + } + }, + { + "key": "src/polyfill-channel-settings.ts|max-params|sendCarousel|0", + "value": 6, + "path": "src/polyfill-channel-settings.ts", + "rule": "max-params", + "scope": "sendCarousel", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/polyfill-channel-settings.ts": "0f6513f22ca02aa9809be547ee83daea2508c082a7118a34df4da5c3b8d39fdc" + } + } + ] +} diff --git a/quality/baselines/duplicates.json b/quality/baselines/duplicates.json new file mode 100644 index 0000000..f32ae4e --- /dev/null +++ b/quality/baselines/duplicates.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "adoptionBase": "b46574a4563dc7cae6a7dfc5b378058c01053aa8", + "entries": [ + { + "key": "typescript|3b0d1d2c147385a1c51f10b414c0d2b850172427221482b7fc1c01a7983ad11f|src/http.ts", + "value": 1, + "paths": [ + "src/http.ts" + ], + "tokens": 52, + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/http.ts": "cff07d8244daba9dae1551c4844f15a4c923bbd3f33ab3cd7a5f67987567a6e8" + } + }, + { + "key": "typescript|e6285bd27db1ab4139f646c8d0b7e9cdee896370ed43b11de68d4b86ec996443|src/http.ts|src/index.ts", + "value": 1, + "paths": [ + "src/http.ts", + "src/index.ts" + ], + "tokens": 61, + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/http.ts": "cff07d8244daba9dae1551c4844f15a4c923bbd3f33ab3cd7a5f67987567a6e8", + "src/index.ts": "83a1db3aa9fcf466fce74f8c6dd22b233f890accadb9140b9b52d8dadc24e047" + } + } + ] +} diff --git a/quality/baselines/unused-full.json b/quality/baselines/unused-full.json new file mode 100644 index 0000000..f8ef944 --- /dev/null +++ b/quality/baselines/unused-full.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": 1, + "adoptionBase": "b46574a4563dc7cae6a7dfc5b378058c01053aa8", + "entries": [ + { + "key": "src/__tests__/mock-sdk.ts|types|MockChatApiClientMocks", + "value": 1, + "path": "src/__tests__/mock-sdk.ts", + "rule": "types", + "symbol": "MockChatApiClientMocks", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/__tests__/mock-sdk.ts": "240a4e2d68e7f02bb0154face80066391e91a01cf06fb9bbd67927ffd61e93ee" + } + }, + { + "key": "src/tools.generated.ts|exports|GENERATED_MCP_TOOL_COUNT", + "value": 1, + "path": "src/tools.generated.ts", + "rule": "exports", + "symbol": "GENERATED_MCP_TOOL_COUNT", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/tools.generated.ts": "42ea1d71d60f5f90ebed4ecc28dc390bee0864eae911de665ba4a168ae9a024f" + } + } + ] +} diff --git a/quality/baselines/unused-production.json b/quality/baselines/unused-production.json new file mode 100644 index 0000000..d559dd7 --- /dev/null +++ b/quality/baselines/unused-production.json @@ -0,0 +1,76 @@ +{ + "schemaVersion": 1, + "adoptionBase": "b46574a4563dc7cae6a7dfc5b378058c01053aa8", + "entries": [ + { + "key": "src/__tests__/mock-sdk.ts|files|src/__tests__/mock-sdk.ts", + "value": 1, + "path": "src/__tests__/mock-sdk.ts", + "rule": "files", + "symbol": "src/__tests__/mock-sdk.ts", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/__tests__/mock-sdk.ts": "240a4e2d68e7f02bb0154face80066391e91a01cf06fb9bbd67927ffd61e93ee" + } + }, + { + "key": "src/auth.ts|exports|extractBearerToken", + "value": 1, + "path": "src/auth.ts", + "rule": "exports", + "symbol": "extractBearerToken", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/auth.ts": "33b9b5e0d89bc6f99366ada3314e9ec0b49a249f06ca29b6cdac39bbcb48c137" + } + }, + { + "key": "src/auth.ts|exports|extractInstanceId", + "value": 1, + "path": "src/auth.ts", + "rule": "exports", + "symbol": "extractInstanceId", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/auth.ts": "33b9b5e0d89bc6f99366ada3314e9ec0b49a249f06ca29b6cdac39bbcb48c137" + } + }, + { + "key": "src/logging.ts|exports|redactForLog", + "value": 1, + "path": "src/logging.ts", + "rule": "exports", + "symbol": "redactForLog", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/logging.ts": "440ae2ef2df7f4e61422dabc7dc2b65c6c9b3250076cbb68660cff39e09ab935" + } + }, + { + "key": "src/tools.generated.ts|exports|GENERATED_MCP_TOOL_COUNT", + "value": 1, + "path": "src/tools.generated.ts", + "rule": "exports", + "symbol": "GENERATED_MCP_TOOL_COUNT", + "owner": "1MSG SDK/tooling maintainers", + "expires": "2026-12-11", + "reason": "Historical findings replayed at the adoption base; each item is tracked independently and cannot increase.", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "evidence": { + "src/tools.generated.ts": "42ea1d71d60f5f90ebed4ecc28dc390bee0864eae911de665ba4a168ae9a024f" + } + } + ] +} diff --git a/quality/mcp/generate.mjs b/quality/mcp/generate.mjs new file mode 100644 index 0000000..62b9a26 --- /dev/null +++ b/quality/mcp/generate.mjs @@ -0,0 +1,149 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { cp, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import YAML from 'yaml'; + +const directory = dirname(fileURLToPath(import.meta.url)); +const root = resolve(directory, '../..'); +const metadata = JSON.parse(await readFile(join(directory, 'provenance.json'), 'utf8')); +/** Hash complete bytes so a producer, contract or SDK-signature edit requires explicit provenance review. */ +const hash = (bytes) => createHash('sha256').update(bytes).digest('hex'); +const producer = join(directory, 'vendor/generate-mcp-tools.cjs'); +assert.equal( + hash(await readFile(producer)), + metadata.producerSha256, + 'Pinned producer checksum mismatch', +); +const specPath = join(directory, 'openapi.yaml'); +const specBytes = await readFile(specPath); +assert.equal(hash(specBytes), metadata.contractSha256, 'Pinned public contract checksum mismatch'); +const spec = YAML.parse(specBytes.toString()); +assert.ok(Object.keys(spec.paths ?? {}).length, 'Pinned contract has no operations'); +const sdk = join(root, 'node_modules/@1msg/sdk'); +assert.equal( + JSON.parse(await readFile(join(sdk, 'package.json'), 'utf8')).version, + metadata.sdkVersion, + 'Published SDK version changed; review compatibility exception and wire coverage', +); +const signatures = join(directory, 'vendor/apis'); +assert.ok(Object.keys(metadata.sdkSignatures).length, 'SDK signature inventory cannot be empty'); +for (const [name, expected] of Object.entries(metadata.sdkSignatures)) { + assert.equal( + hash(await readFile(join(signatures, name))), + expected, + `SDK signature changed: ${name}`, + ); +} + +/** Read operation parameter lists from a generated SDK interface without executing the client or making requests. */ +function parameterLists(source) { + const body = source.match(/export interface \w+ApiInterface \{([\s\S]*?)\n\}/)?.[1]; + assert.ok(body, 'SDK operation interface missing'); + const result = {}; + for (const match of body.matchAll(/^\s+(\w+)\(([^)]*)\):/gm)) { + const [, name, parameters] = match; + if (name.endsWith('Raw') || name.endsWith('RequestOpts')) continue; + result[name] = parameters + .split(',') + .map((parameter) => parameter.trim().split(':')[0]) + .filter((name) => name && !name.startsWith('initOverrides')); + } + return result; +} + +const exception = JSON.parse( + await readFile(join(directory, 'sdk-compatibility-exception.json'), 'utf8'), +); +assert.ok( + exception.owner && exception.issue, + 'SDK compatibility exception requires accountable ownership', +); +assert.ok( + Date.parse(exception.expires + 'T23:59:59Z') >= Date.now(), + 'Published SDK compatibility exception expired', +); +const gaps = []; +for (const name of Object.keys(metadata.sdkSignatures)) { + const expected = parameterLists(await readFile(join(signatures, name), 'utf8')); + const actual = parameterLists( + await readFile(join(sdk, 'dist/generated/src/apis', name.replace('.ts', '.d.ts')), 'utf8'), + ); + for (const [method, parameters] of Object.entries(expected)) { + if (JSON.stringify(actual[method]) !== JSON.stringify(parameters)) + gaps.push({ + api: name.replace('.ts', ''), + method, + expectedParameters: parameters, + publishedParameters: actual[method] ?? null, + }); + } +} +assert.deepEqual( + gaps, + exception.findings, + 'Published SDK compatibility drift: review each operation instead of expanding an implicit exemption', +); +console.log( + `Known SDK compatibility exception: ${gaps.length} exact parameter-list gaps, expires ${exception.expires}`, +); + +/** Inventory every generated TypeScript file on disk, including ignored and untracked files. */ +async function inventory(source) { + const files = (await readdir(source)).filter((name) => name.endsWith('.generated.ts')).sort(); + const result = {}; + for (const file of files) result[file] = hash(await readFile(join(source, file))); + return result; +} + +const temporary = await mkdtemp(join(tmpdir(), '1msg-mcp-generation-')); +try { + await mkdir(join(temporary, 'gen'), { recursive: true }); + await mkdir(join(temporary, 'public'), { recursive: true }); + await mkdir(join(temporary, 'packages/sdk/src/generated/src/apis'), { recursive: true }); + await mkdir(join(temporary, 'packages/mcp/src'), { recursive: true }); + await cp(producer, join(temporary, 'gen/generate-mcp-tools.cjs')); + await cp(specPath, join(temporary, 'public/openapi.bundled.yaml')); + await symlink(join(root, 'node_modules'), join(temporary, 'node_modules'), 'dir'); + for (const name of Object.keys(metadata.sdkSignatures)) { + await cp(join(signatures, name), join(temporary, 'packages/sdk/src/generated/src/apis', name)); + } + const execution = spawnSync(process.execPath, ['gen/generate-mcp-tools.cjs'], { + cwd: temporary, + encoding: 'utf8', + }); + assert.equal( + execution.status, + 0, + `MCP producer failed: ${execution.stdout}\n${execution.stderr}`, + ); + const generated = join(temporary, 'packages/mcp/src'); + const expected = await inventory(generated); + assert.equal(Object.keys(expected).length, 2, 'MCP producer must emit both tools and handlers'); + const manifest = join(directory, 'inventory.json'); + if (process.argv.includes('--write')) { + for (const file of Object.keys(await inventory(join(root, 'src')))) { + if (!(file in expected)) await rm(join(root, 'src', file)); + } + for (const file of Object.keys(expected)) + await cp(join(generated, file), join(root, 'src', file)); + await writeFile(manifest, JSON.stringify(expected, null, 2) + '\n'); + } else { + for (const actual of [ + await inventory(join(root, 'src')), + JSON.parse(await readFile(manifest, 'utf8')), + ]) { + assert.deepEqual( + actual, + expected, + 'Generated drift: review producer and run node quality/mcp/generate.mjs --write', + ); + } + } + console.log('Verified real MCP generation from pinned producer, contract and SDK signatures'); +} finally { + await rm(temporary, { recursive: true, force: true }); +} diff --git a/quality/mcp/inventory.json b/quality/mcp/inventory.json new file mode 100644 index 0000000..14aa89f --- /dev/null +++ b/quality/mcp/inventory.json @@ -0,0 +1,4 @@ +{ + "handlers.generated.ts": "72cbda7f1561b58355e9fccee0a4d029464f66c0a7e3827a36aa24a3adadbae6", + "tools.generated.ts": "786fbe9d283503fad1ec1faaff65fc68cfb6fb835b494ffd238dd86611e8d8f6" +} diff --git a/quality/mcp/openapi.yaml b/quality/mcp/openapi.yaml new file mode 100644 index 0000000..ebed642 --- /dev/null +++ b/quality/mcp/openapi.yaml @@ -0,0 +1,4621 @@ +openapi: 3.0.3 +info: + title: 1MSG WhatsApp Business API (Public) + description: > + Public client API for sending messages, managing groups, flows, and + templates. + + + This is the API exposed to end customers via platform.1msg.io + + + **Authentication:** Pass the channel token in **one** place. The API checks + + JSON body (`token`), then query string (`?token=`), then + + `Authorization: Bearer` header (preferred in production). + + + **Documentation:** + https://help.1msg.io/platform-1msg/getting-start/quick-start + + **API Docs:** https://docs.1msg.io/ + version: 1.0.0 + contact: + name: 1MSG API Support + url: https://help.1msg.io + email: support@1msg.io +servers: + - url: https://api.1msg.io + description: Production + - url: https://api-stage.1msg.io + description: Staging + - url: http://127.0.0.1:3050 + description: Local development +tags: + - name: Messaging + description: Send messages, files, templates, and other content + - name: Users + description: Block, unblock, and list blocked WhatsApp users + - name: Profile + description: Channel profile information (read and update) + - name: Channel + description: Channel status and automation settings + - name: Catalog + description: Catalog and cart commerce settings (`GET`/`POST /commerce`) + - name: Groups + description: Create and manage WhatsApp groups + - name: Flows + description: WhatsApp Flows (interactive forms) + - name: Templates + description: Get and manage message templates + - name: Webhooks + description: > + Client webhook URL configuration (`GET`/`POST /webhook`). + + + Auto-disable is **per URL**, not per channel. Prefer `webhookStatuses` on + + `GET /webhook` / `GET /settings`. Top-level `haveDisabledWebhooks` means + at + + least one configured URL is sticky-disabled; live URLs still receive + events. + - name: Calling + description: > + WhatsApp Calling API (beta) — VoIP **signaling** proxy. + + + 1msg does **not** host media, store call history, or store recordings. + + You need your own WebRTC or SIP stack. + + + ### Prerequisites + + - Meta Calling enabled on the number (Cloud API; not COEX) + + - Paid channel — trial / `subscriptionBlocked` → **403** plain text + + - Webhook URL configured (`POST /webhook`) for `calls` events + + + ### Inbound (user → business) + + 1. Webhook `type=calls`, `event=connect` — take `call_id` + SDP **offer** + + 2. Optional: `POST /initiateCall` `action=pre_accept` + WebRTC **answer** + SDP + + 3. `action=accept` + WebRTC **answer** SDP, or `action=reject` + + 4. Later `event=terminate`, or `action=terminate` to hang up + + + Answer within ~**30–60s** or Meta ends as unanswered. + + + **Critical:** `accept` / `pre_accept` need a **WebRTC-generated SDP + answer**. + + Do **not** echo Meta's offer. Postman/curl alone cannot establish real + media. + + + ### Outbound (business → user) + + 1. Send Call Permission Request (CPR) — template or interactive + + 2. Wait for permission reply on the **messages** webhook + (`call_permission_reply`) + + 3. `POST /initiateCall` `action=connect` + business SDP **offer** + + 4. Apply Meta's SDP **answer** from the `calls` connect webhook + + + Meta limits (summary): permission ~7 days; up to 5 calls / 24h / user; + + max 2 CPR / 7 days; auto-revoke after 4 unanswered. + + + ### Webhooks (`type=calls`) + + Passthrough of Meta `field=calls` plus `type`, `instanceId`. + + Events: connect (offer or answer SDP), statuses (`RINGING` / `ACCEPTED` / + + `REJECTED`), terminate. CPR replies use the messages path, not + `type=calls`. + + + See operations below for request bodies and examples. +paths: + /me: + get: + summary: Get Profile Info + description: > + Return the WhatsApp Business profile for this channel: About, address, + + description, email, phone, websites, industry (`vertical`), and profile + photo URL. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Profile information + content: + application/json: + schema: + $ref: "#/components/schemas/ProfileInfo" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Profile + operationId: getMe + post: + summary: Update profile info + description: > + Update WhatsApp Business Account profile fields. At least one of + + about, description, email, photo, address, vertical, websites is + required. + + Blocked when the channel subscription limit is exceeded. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + about: + type: string + description: Profile About section (WhatsApp status text) + example: Available 9–18 + address: + type: string + description: Business address shown on the profile + description: + type: string + description: Business description (max 256 characters) + email: + type: string + description: Business email shown on the profile + example: support@example.com + vertical: + type: string + description: Industry of the business + photo: + type: string + description: HTTPS URL or data:image/...;base64,... payload + websites: + type: array + items: + type: string + description: Business website URLs + additionalProperties: true + example: + about: Available 9–18 + email: support@example.com + responses: + "200": + description: Profile updated + content: + application/json: + schema: + type: object + properties: + result: + type: string + example: success + additionalProperties: true + example: + result: success + "401": + $ref: "#/components/responses/InvalidToken" + "422": + description: Wrong request (no updatable fields) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Profile + operationId: updateMe + /sendMessage: + post: + summary: Send a Message + description: > + Send a message to an existing chat. (Only if the dialogue has an Open + Session). + + The message will be added to the queue for sending and delivered even if + the phone + + is disconnected from the Internet or authorization is not passed. + + + Only one of two parameters is needed to determine the destination - + chatId or phone. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/SendMessageRequest" + application/json: + schema: + $ref: "#/components/schemas/SendMessageRequest" + example: + body: Hello World + phone: 12020721369 + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendMessage + /sendFile: + post: + summary: Send a File + description: > + Send a file to an existing chat. (Only if the dialogue has an Open + Session). + + Only one of two parameters is needed to determine the destination - + chatId or phone. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/SendFileRequest" + application/json: + schema: + $ref: "#/components/schemas/SendFileRequest" + example: + phone: 12020721369 + body: https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf + filename: dummy.pdf + caption: Here is the file + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendFile + /sendLocation: + post: + summary: Send a Location + description: > + Send a location to an existing chat. (Only if the dialogue has an Open + Session). + + Only one of two parameters is needed to determine the destination - + chatId or phone. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - lat + - lng + properties: + lat: + type: string + description: "Latitude of the location. Example: 45.018337" + lng: + type: string + description: "Longitude of the location. Example: -73.968285" + address: + type: string + description: "Address of the location. Only displayed if name is present. + Example: 9766 Valley View St., New York, NY 10024" + name: + type: string + description: "Name of the location. Example: Facebook HQ" + quotedMsgId: + type: string + description: Quoted message ID (Cloud API) + chatId: + type: string + description: "Required if phone is not set. Chat ID from the message list. + Examples: 12020721369@c.us or + 120363046942338209@g.us(group)" + phone: + type: integer + description: "Required if chatId is not set. A phone number starting with the + country code. USA example: 12020721369." + application/json: + schema: + type: object + required: + - lat + - lng + properties: + lat: + type: string + lng: + type: string + address: + type: string + name: + type: string + quotedMsgId: + type: string + chatId: + type: string + phone: + type: integer + example: + lat: "45.018337" + lng: "-73.968285" + name: Facebook HQ + address: 9766 Valley View St., New York, NY 10024 + phone: 12020721369 + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendLocation + /sendContact: + post: + summary: Send a Contact + description: > + Send a WhatsApp contact card. + + + Preferred simple body (legacy-compatible): + + + ```json + + { + "phone": "79001234567", + "contact": { "name": "Lida", "phone": "+79181976551" } + } + + ``` + + + Cloud API shape also works: `contacts` is an array of objects with + + `name.formatted_name` and `phones[].phone` (E.164 string, **not** a + timestamp). + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + requestBody: + content: + application/json: + schema: + type: object + properties: + phone: + type: string + description: Recipient phone with country code. Use phone or chatId. + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + quotedMsgId: + type: string + contact: + type: object + description: Simple contact card. Use this or `contacts`. + required: + - name + - phone + properties: + name: + description: Display name (string) or Cloud API name object + oneOf: + - type: string + - type: object + additionalProperties: true + phone: + type: string + description: Contact phone in E.164 (`+79181976551`), not a timestamp + email: + type: string + org: + type: object + additionalProperties: true + url: + type: string + contacts: + type: array + items: + type: object + properties: + name: + type: object + properties: + formatted_name: + type: string + first_name: + type: string + last_name: + type: string + middle_name: + type: string + suffix: + type: string + prefix: + type: string + birthday: + type: string + addresses: + type: array + items: + type: object + additionalProperties: true + emails: + type: array + items: + type: object + additionalProperties: true + org: + type: object + properties: + company: + type: string + department: + type: string + title: + type: string + phones: + type: array + items: + type: object + properties: + phone: + type: string + description: E.164 phone (`+79181976551`). Not a timestamp. + type: + type: string + wa_id: + type: string + urls: + type: array + items: + type: object + additionalProperties: true + example: + phone: "{{testPhone}}" + contact: + name: John Doe + phone: "+1234567890" + operationId: sendContact + /sendList: + post: + summary: Send List Message + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + requestBody: + content: + application/json: + schema: + type: object + properties: + phone: + type: string + description: Recipient phone with country code. Use phone or chatId. + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + quotedMsgId: + type: string + body: + type: string + buttonText: + type: string + action: + type: string + sections: + type: array + items: + type: object + properties: + title: + type: string + rows: + type: array + items: + type: object + properties: + id: + type: string + format: utc-millisec + title: + type: string + description: + type: string + example: + phone: "{{testPhone}}" + body: This is a list message + buttonText: Click me + action: Action + sections: + - title: Section 1 + rows: + - id: "1" + title: Row 1 + description: Description for Row 1 + operationId: sendList + /sendButton: + post: + summary: Send Reply Buttons Message + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + requestBody: + content: + application/json: + schema: + type: object + properties: + phone: + type: string + description: Recipient phone with country code. Use phone or chatId. + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + quotedMsgId: + type: string + description: Optional Cloud API wamid to quote + body: + type: string + footer: + type: string + header: + type: object + additionalProperties: true + description: Optional interactive header (text/image/video/document) + buttons: + type: array + items: + type: object + description: Legacy alias for sections (`[{id,title}]`) + sections: + type: array + items: + type: object + properties: + type: + type: string + reply: + type: object + properties: + id: + type: string + format: utc-millisec + title: + type: string + example: + phone: "{{testPhone}}" + body: This is a buttons message + footer: Footer + sections: + - type: reply + reply: + id: "1" + title: Button 1 + operationId: sendButton + /sendLocationRequest: + post: + summary: Send Location Request Message + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + requestBody: + content: + application/json: + schema: + type: object + properties: + phone: + type: string + description: Recipient phone with country code. Use phone or chatId. + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + quotedMsgId: + type: string + body: + type: string + example: + phone: "{{testPhone}}" + body: Text + operationId: sendLocationRequest + /sendFlow: + post: + summary: Send WhatsApp Flow Message + description: > + Send Interactive WhatsApp Flow message to an existing chat. (Only if the + dialogue has an Open Session). + + Only one of two parameters is needed to determine the destination - + chatId or phone. + + + Use this method to send a published WhatsApp Flow as a service + (interactive) message. + + If the 24-hour window is closed, send a template with a FLOW button via + /sendTemplate. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - body + - flowId + - flowToken + - flowCta + properties: + body: + type: string + description: Flow message body text + header: + oneOf: + - type: string + - type: object + description: Flow header (string or text header object) + footer: + type: string + description: Footer text + flowId: + type: string + description: Published Flow ID + flowToken: + type: string + description: Flow token generated by the business + flowCta: + type: string + description: CTA button text + flowAction: + type: string + enum: + - navigate + - data_exchange + description: Flow action type + flowActionPayload: + type: object + description: Required for flowAction=navigate (screen is required). Ignored for + data_exchange. If data is provided, it must be a non-empty + object. + flowMessageVersion: + type: string + description: Flow message version (default "3") + mode: + type: string + enum: + - draft + - published + description: Flow mode (draft or published). If omitted, provider default + applies + flowActionData: + type: object + description: Shortcut for flowActionPayload.data (optional) + flowActionScreen: + type: string + description: Shortcut for flowActionPayload.screen (optional) + quotedMsgId: + type: string + description: Quoted message ID (Cloud API) + chatId: + type: string + description: "Required if phone is not set. Chat ID from the message list. + Examples: 12020721369@c.us or + 120363046942338209@g.us(group)" + phone: + type: integer + description: "Required if chatId is not set. A phone number starting with the + country code. USA example: 12020721369." + application/json: + schema: + type: object + required: + - body + - flowId + - flowToken + - flowCta + properties: + body: + type: string + header: + oneOf: + - type: string + - type: object + footer: + type: string + flowId: + type: string + flowToken: + type: string + flowCta: + type: string + flowAction: + type: string + enum: + - navigate + - data_exchange + flowActionPayload: + type: object + flowMessageVersion: + type: string + mode: + type: string + enum: + - draft + - published + flowActionData: + type: object + flowActionScreen: + type: string + quotedMsgId: + type: string + chatId: + type: string + phone: + type: integer + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendFlow + /sendReaction: + post: + summary: Send Reaction + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + requestBody: + content: + application/json: + schema: + type: object + properties: + phone: + type: string + description: Recipient phone with country code. Use phone or chatId. + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + body: + type: string + quotedMsgId: + type: string + example: + phone: "{{testPhone}}" + body: 😀 + quotedMsgId: "{{lastMessageId}}" + operationId: sendReaction + /sendCarousel: + post: + summary: Send Carousel + description: > + You can send product cards via Carousel in two ways: + + + Template messages: do not require a 24-hour customer service window + between you and the recipient. Use sendTemplate. + + + Free-form messages: can be sent only when a customer service window is + open between you and the recipient. Use sendCarousel. + + + The message structure in /sendCarousel is largely similar to sending a + template. However, in this case you must + + explicitly specify all elements that are created in advance when working + with templates. This is because the message + + is sent without using a template. + + + In /sendCarousel, for a Catalog Carousel there can be either 1 URL + button or one or more quick reply buttons. + + + Prefer the simple `cards` array (2–10 cards). Each card needs a header + media/product + + plus a CTA URL or quick-reply buttons. Example: + + + ```json + + { + "phone": "79001234567", + "body": "Pick a product", + "cards": [ + { + "header": { "type": "image", "image": { "link": "https://example.com/1.jpg" } }, + "body": { "text": "Card one" }, + "type": "cta_url", + "action": { "name": "cta_url", "parameters": { "url": "https://example.com/p1", "display_text": "Open" } } + }, + { + "header": { "type": "image", "image": { "link": "https://example.com/2.jpg" } }, + "body": { "text": "Card two" }, + "type": "cta_url", + "action": { "name": "cta_url", "parameters": { "url": "https://example.com/p2", "display_text": "Open" } } + } + ] + } + + ``` + + + `params` with a `CAROUSEL` component is the template-like alternative. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + body: + type: string + description: Text shown above the carousel. Optional. If omitted and params + include a body component, the body will be taken from + params. + cards: + type: array + minItems: 2 + maxItems: 10 + description: Preferred. 2–10 carousel cards (header + body + CTA or quick + replies). + items: + type: object + additionalProperties: true + params: + type: array + items: + type: object + description: > + Required. Template-like structure (same as sendTemplate + params). Must include a CAROUSEL component and its cards. + + + Structure: + + - params[] item with type CAROUSEL and cards (2..10) + + - cards[] item: + - components[] items: + - header: parameters[0] with type image/video/product + - image: {"image": {"link": "https://..."}} + - video: {"video": {"link": "https://..."}} + - product: {"product": {"catalog_id": "...", "product_retailer_id": "..."}} + - button: sub_type url or quick_reply + - url: parameters {"text": "Button", "url": "https://..."} + - quick_reply: parameters[] of {"id": "...", "text": "..."} + quotedMsgId: + type: string + description: Quoted message ID (Cloud API) + chatId: + type: string + description: "Required if phone is not set. Chat ID from the message list. + Examples: 12020721369@c.us or + 120363046942338209@g.us(group)" + phone: + type: integer + description: "Required if chatId is not set. A phone number starting with the + country code. USA example: 12020721369." + application/json: + schema: + type: object + properties: + body: + type: string + description: Text shown above the carousel + cards: + type: array + minItems: 2 + maxItems: 10 + description: Preferred. 2–10 carousel cards. + items: + type: object + additionalProperties: true + params: + type: array + items: + type: object + description: Template-like alternative — include a CAROUSEL component with cards + quotedMsgId: + type: string + chatId: + type: string + phone: + type: integer + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendCarousel + /sendProduct: + post: + summary: Send a Product + description: > + Send a single catalog product or a product list. + + Destination is `chatId` or `phone`. `action` is required: + + - single product: `{ "catalog_id": "...", "product_retailer_id": "..." + }` + + - product list: `{ "catalog_id": "...", "sections": [{ "title": "...", + "product_items": [{ "product_retailer_id": "..." }] }] }` + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - action + properties: + phone: + type: string + description: Recipient phone with country code. Use phone or chatId. + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + quotedMsgId: + type: string + body: + type: string + description: Optional message text shown with the product + footer: + type: string + header: + type: string + description: Text header (product list only) + action: + type: object + additionalProperties: true + description: Catalog action (`catalog_id` + `product_retailer_id`, or `sections` + for a list) + additionalProperties: true + example: + chatId: 12020721369@c.us + body: Have a look + action: + catalog_id: "448008021021719" + product_retailer_id: "0001" + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendProduct + /sendTemplate: + post: + summary: Send Template Message + description: > + Send a WhatsApp **template** message (works outside the 24-hour session + window). + + + Use this tool — not `send_message` — when the 24h session is closed. + + + `params` MUST be Cloud API **component objects**, never a flat list of + strings + + and never the old On-Prem `hsm` / `localizable_params` format. + + + Example body variable `{{1}}`: + + + ```json + + { + "type": "body", + "parameters": [{ "type": "text", "text": "Ivan" }] + } + + ``` + + + Do not send `"type": "hsm"` and do not send `params: ["Ivan", "123"]`. + + + Supported `params` button `sub_type` values include: `url`, + `quick_reply`, + + `copy_code` / `coupon_code`, `catalog`, `flow`, `limited_time_offer`, + and + + **`order_details`** (WhatsApp Payments **India only** — requires an + approved + + Utility template with an ORDER_DETAILS button). + + + Convenience wrapper with structured fields: `POST /sendOrderDetails`. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Templates + requestBody: + content: + application/json: + schema: + type: object + properties: + template: + type: string + description: Approved template name (template `name`, not the old HSM + `element_name`) + language: + type: object + description: Template language. Always set `policy` to `deterministic`. + properties: + policy: + type: string + description: Must be `deterministic` + example: deterministic + default: deterministic + code: + type: string + example: en + namespace: + type: string + description: Template namespace from `list_templates` + params: + type: array + description: > + Template components in WhatsApp Cloud API format. + + Each item is an object with `type` (`body`, `header`, + `button`) + + and `parameters` (objects with `type` + `text` / media / + action). + + Never a list of strings. Never HSM `localizable_params`. + items: + type: object + additionalProperties: true + description: | + Component object, e.g. {"type":"body","parameters":[{"type":"text","text":"Ivan"}]} or a header/button/carousel component. Not a string. + phone: + type: string + description: Recipient phone with country code (use phone or chatId) + chatId: + type: string + description: Recipient chat ID (`phone@c.us`, group `@g.us`, or BSUID `@lid`). + Use phone or chatId. + quotedMsgId: + type: string + description: Optional Cloud API wamid to quote + useMMlite: + type: boolean + description: > + Force Marketing Messages API (POST marketing_messages). + + If omitted: auto for MARKETING category when channel has + mm_lite_enabled + + and mm_lite_available service settings. + messageActivitySharing: + type: boolean + description: > + Sets message_activity_sharing on WABA payload. Enables click + tracking webhooks. + + Requires MM Lite path. Ignored on Cloud API fallback. + messageSendTtlSeconds: + type: integer + description: > + Template message TTL in seconds (message_send_ttl_seconds). + + MARKETING via MM Lite: 43200–2592000. AUTHENTICATION: + 30–900. + + UTILITY: 30–43200. AUTH/UTILITY also accept -1 (30-day + custom TTL). + example: + template: test_buttons_2 + language: + policy: deterministic + code: ru + namespace: 41123797_e45e_4f06_a2fa_443b27c6f5ec + phone: "{{testPhone}}" + params: + - type: body + parameters: + - type: text + text: Ivan + operationId: sendTemplate + /messages: + get: + summary: Get messages list + description: | + List stored chat history for this channel (requires history storage). + Filter with `chatId`, `limit`, `last`, message numbers, or time bounds. + parameters: + - $ref: "#/components/parameters/token" + - name: chatId + in: query + schema: + type: string + description: Filter messages for one chat (`phone@c.us`, group `@g.us`, or BSUID + `@lid`) + - name: limit + in: query + schema: + type: integer + description: Max rows to return (default 100) + - name: last + in: query + schema: + type: boolean + description: If true, return the latest messages (DESC). Default false. + - name: lastMessageNumber + in: query + schema: + type: integer + - name: firstMessageNumber + in: query + schema: + type: integer + - name: min_time + in: query + schema: + type: integer + description: Unix timestamp lower bound + - name: max_time + in: query + schema: + type: integer + description: Unix timestamp upper bound + - name: msgId + in: query + schema: + type: string + description: Filter by stored message id + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + notice: + type: string + messages: + type: array + items: + type: object + additionalProperties: true + additionalProperties: true + examples: + ok: + value: + messages: [] + historyDisabled: + value: + notice: saving history disabled + tags: + - Messaging + operationId: listMessages + /readMessage: + post: + summary: Mark message as read + parameters: + - $ref: "#/components/parameters/token" + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + messageId: + type: string + msgId: + type: string + typingIndicator: + type: boolean + default: false + description: Show WhatsApp typing indicator (max 25s or until reply) + application/json: + schema: + type: object + properties: + messageId: + type: string + msgId: + type: string + typingIndicator: + type: boolean + default: false + description: Show WhatsApp typing indicator (max 25s or until reply) + responses: + "200": + description: Server response example + content: + application/json: + schema: + type: object + properties: + result: + type: string + additionalProperties: true + example: + result: success + "401": + description: Invalid token + content: + application/json: + schema: + type: object + properties: + error: + type: string + tags: + - Messaging + operationId: createReadMessage + /uploadMedia: + post: + summary: Upload media + description: > + Upload a file to WhatsApp media storage and get a numeric `mediaId` + + for later use with `send_file` / `send_sticker` (`mediaId` field, not + `body`). + + + Pass the file as `body` **or** `url` — an HTTP(S) URL or a base64 data + URI. + + The `mediaId` in the response is a numeric string (keep it as a string). + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + body: + type: string + description: File HTTP(S) URL or base64 data URI (`data:;base64,...`) + url: + type: string + description: Alias for `body` — public HTTP(S) URL of the file to upload + example: + body: https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + mediaId: + type: string + additionalProperties: true + tags: + - Messaging + operationId: createUploadMedia + /sendSticker: + post: + summary: Send sticker message + description: Send a WhatsApp sticker by mediaId or link URL. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + phone: + type: integer + chatId: + type: string + mediaId: + type: string + description: WABA media id from uploadMedia + link: + type: string + description: Public URL to webp sticker + quotedMsgId: + type: string + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendSticker + /sendCtaUrl: + post: + summary: Send CTA URL interactive message + description: Send an interactive message with a single call-to-action URL button. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - body + - displayText + - url + properties: + phone: + type: integer + chatId: + type: string + body: + type: string + displayText: + type: string + description: Button label + url: + type: string + format: uri + header: + type: object + additionalProperties: true + footer: + type: string + quotedMsgId: + type: string + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendCtaUrl + /sendAddressMessage: + post: + summary: Send address request message + description: > + Request shipping address from the user (WhatsApp interactive + `address_message`). + + + **India and Singapore only.** Requires: + + - Business WhatsApp number registered in that country + + - Recipient phone matching the country (`+91` ↔ `IN`, `+65` ↔ `SG`) + + + Pass `country: "IN"` or `country: "SG"`. Eligibility is validated + upstream; + + mismatches (e.g. Singapore phone with `country: "IN"`) return errors + such as + + `Unsupported Interactive Message type` (HTTP 200 with `sent: false`). + + + Optional action parameters: `values`, `saved_addresses`, + `validation_errors`. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - body + properties: + phone: + type: integer + description: Recipient phone (E.164 digits, no +). Must match country. + chatId: + type: string + body: + type: string + description: Body text shown with the address request + country: + type: string + enum: + - IN + - SG + description: Address form country. Defaults to IN if omitted. + example: SG + values: + type: object + description: Optional prefilled address fields + additionalProperties: true + saved_addresses: + type: array + description: Optional previously saved addresses for the user + items: + type: object + additionalProperties: true + validation_errors: + type: object + description: Optional field validation errors when re-prompting + additionalProperties: true + quotedMsgId: + type: string + additionalProperties: true + examples: + singapore: + summary: Singapore + value: + phone: 6531650115 + body: Thanks for your order! Tell us what address you’d like this delivered to. + country: SG + india: + summary: India + value: + phone: 919876543210 + body: Thanks for your order! Tell us what address you’d like this delivered to. + country: IN + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendAddressMessage + /sendOrderDetails: + post: + summary: Send order details (India payments template) + description: > + Send a WhatsApp **order details** payment / invoice message using a + + pre-approved **Utility** template that has an `ORDER_DETAILS` button. + + + **India only** (WhatsApp Payments India). Requires: + + - India WhatsApp Business number + + - Commerce enabled on the channel (`GET`/`POST /commerce`) + + - Approved template with an `ORDER_DETAILS` button + + + Use this method when you need structured fields (`order`, `referenceId`, + + `currency`, `paymentSettings`). The API appends a template button + + `sub_type: order_details` and sends via the same path as `POST + /sendTemplate`. + + + Works **outside the 24-hour session window** (template message). + + + You can also send the same payload yourself with `POST /sendTemplate` by + + including a button component in `params`: + + + ```json + + { + "type": "button", + "sub_type": "order_details", + "index": 0, + "parameters": [{ + "type": "action", + "action": { + "order_details": { + "reference_id": "order-123", + "currency": "INR", + "order": { "status": "pending", "items": [], "subtotal": { "offset": 100, "value": 50000 } } + } + } + }] + } + + ``` + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - template + - namespace + - language + - order + properties: + phone: + type: integer + description: Recipient phone (India E.164 digits, no +). Use phone or chatId. + example: 919876543210 + chatId: + type: string + description: Recipient chatId (e.g. phone@c.us). Use phone or chatId. + template: + type: string + description: Approved Utility template name that includes an ORDER_DETAILS + button + example: order_details_utility + namespace: + type: string + description: Template namespace from the channel / template list + example: your_namespace_uuid + language: + type: object + description: Template language + required: + - code + properties: + code: + type: string + description: Language code + example: en + policy: + type: string + description: Optional language policy + example: deterministic + additionalProperties: true + params: + type: array + description: > + Extra template components (HEADER / BODY / etc.). If an + order_details button is missing, the API appends one from + order / referenceId / currency / paymentSettings. + items: + type: object + additionalProperties: true + referenceId: + type: string + description: Unique order / payment reference id (maps to reference_id) + example: order-123 + currency: + type: string + description: Currency code for India payments + example: INR + paymentSettings: + type: object + description: > + Optional payment settings (UPI / payment gateway / payment + link). Forwarded as payment_settings on the order_details + action. + additionalProperties: true + order: + type: object + description: > + Order payload for the ORDER_DETAILS button. Typical fields: + status, items[], subtotal, tax, shipping, discount. Amount + objects use `{ "offset": 100, "value": }` + (e.g. value 50000 with offset 100 = ₹500.00). + required: + - items + properties: + status: + type: string + description: Order status + example: pending + items: + type: array + description: Line items + items: + type: object + properties: + retailer_id: + type: string + example: SKU-1 + name: + type: string + example: Item + quantity: + type: integer + example: 1 + amount: + type: object + properties: + offset: + type: integer + example: 100 + value: + type: integer + example: 50000 + additionalProperties: true + additionalProperties: true + subtotal: + type: object + properties: + offset: + type: integer + example: 100 + value: + type: integer + example: 50000 + additionalProperties: true + tax: + type: object + additionalProperties: true + shipping: + type: object + additionalProperties: true + discount: + type: object + additionalProperties: true + additionalProperties: true + additionalProperties: true + example: + phone: 919876543210 + template: order_details_utility + namespace: your_namespace_uuid + language: + code: en + referenceId: order-123 + currency: INR + order: + status: pending + items: + - retailer_id: SKU-1 + name: Item + amount: + offset: 100 + value: 50000 + quantity: 1 + subtotal: + offset: 100 + value: 50000 + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendOrderDetails + /sendPaymentRequest: + post: + summary: Send payment request (regional) + description: > + Send a regional payment request interactive message (beta scaffold). + + `region` must be IN, SG, or BR. Payload shape follows Meta regional + payments docs; + + verify on stage before production use. Full regional builders are not + implemented yet. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - phone + - region + properties: + phone: + type: integer + region: + type: string + enum: + - IN + - SG + - BR + body: + type: string + interactive: + type: object + additionalProperties: true + action: + type: object + additionalProperties: true + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/MessageSent" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: sendPaymentRequest + /retrieveMedia: + get: + summary: Retrieve uploaded media metadata + description: | + Get WABA media URL and metadata by mediaId (from uploadMedia). + The returned `url` is temporary and typically expires within ~5 minutes. + parameters: + - $ref: "#/components/parameters/token" + - name: mediaId + in: query + required: true + schema: + type: string + responses: + "200": + description: Media metadata + content: + application/json: + schema: + type: object + properties: + url: + type: string + mimeType: + type: string + sha256: + type: string + fileSize: + type: integer + id: + type: string + additionalProperties: true + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: retrieveMedia + /media/{mediaId}: + delete: + summary: Delete media from WABA storage + description: > + Delete previously uploaded media by numeric `mediaId` (from + `/uploadMedia`). + + + This is the canonical deletion endpoint and uses the REST `DELETE` verb + on the + + media resource path. The older `POST /deleteMedia` is a deprecated + alias. + parameters: + - $ref: "#/components/parameters/token" + - name: mediaId + in: path + required: true + description: Numeric WABA media id + schema: + type: string + responses: + "200": + $ref: "#/components/responses/Success" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: deleteMedia + /deleteMedia: + post: + summary: Delete media from WABA storage (deprecated alias) + deprecated: true + description: > + **Deprecated.** Use `DELETE /media/{mediaId}` instead. + + + This POST alias is kept for backward compatibility with earlier + integrations. + + New integrations should call `DELETE /media/{mediaId}`: 1msg follows + REST + + conventions for resource deletion going forward (delete a resource with + the + + `DELETE` verb on its resource path). + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - mediaId + properties: + mediaId: + type: string + description: Numeric WABA media id + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/Success" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: deleteMediaLegacy + /blockUser: + post: + summary: Block WhatsApp user + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - phone + properties: + phone: + type: integer + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/Success" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Users + operationId: blockUser + /unblockUser: + post: + summary: Unblock WhatsApp user + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - phone + properties: + phone: + type: integer + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/Success" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Users + operationId: unblockUser + /blockedUsers: + get: + summary: List blocked WhatsApp users + description: > + Returns users currently blocked on this WhatsApp channel (WABA `GET + /block_users`). + + Same channel token auth as `blockUser` / `unblockUser`. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Blocked users list + content: + application/json: + schema: + type: object + properties: + blockedUsers: + type: array + items: + type: object + additionalProperties: true + additionalProperties: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Users + operationId: listBlockedUsers + /mmLiteStatus: + get: + summary: Get MM Lite availability and status + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: MM Lite status for the channel + content: + application/json: + schema: + type: object + properties: + available: + type: boolean + status: + type: string + message: + type: string + additionalProperties: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Messaging + operationId: getMmLiteStatus + /status: + get: + summary: Get channel status + description: > + Return WhatsApp Cloud API connection status for this channel + (`connected` + + when the number can send). May include `accountStatus`, messaging limit, + + and MM Lite availability. + + + Optional `health_status_warning` appears only when Meta Graph health + could + + not be loaded and a soft `connected` response is returned. Omitted on + the + + normal path and when health details are not applicable for the channel — + + never an internal provider/transport name. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Channel status + content: + application/json: + schema: + type: object + properties: + isCloud: + type: boolean + example: true + status: + type: string + description: typically `connected` when the number can send + example: connected + accountStatus: + type: string + example: authenticated + mm_lite_available: + type: boolean + health_status_warning: + type: string + description: Optional soft-fail warning when Meta Graph health_status could not + be loaded. Never includes internal provider names. + additionalProperties: true + example: + isCloud: true + status: connected + accountStatus: authenticated + mm_lite_available: false + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Channel + operationId: getStatus + /settings: + get: + summary: Get channel settings + description: > + Return channel settings: webhook URL(s), delivery flags, and analytics + toggles. + + + Auto-disable / circuit state matches `GET /webhook`. **Prefer + `webhookStatuses`** + + (one row per configured URL). The breaker is per URL, not per channel. + + + Channel-level `haveDisabledWebhooks` is `true` when **at least one** URL + in + + `webhookUrl` is sticky auto-disabled — not "the channel is dead". Live + URLs + + still receive events. Top-level `circuitState` / `reason` / `fails` / + `trips` + + are a compatibility summary of one URL (first sticky-disabled, else + first + + configured). + + + This is the same `GET /settings` endpoint as the REST API (previously + missing from MCP). + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Current settings + content: + application/json: + schema: + type: object + properties: + webhookUrl: + oneOf: + - type: string + - type: array + items: + type: string + description: One URL, or several if multiple webhooks are configured + guaranteedHooks: + type: boolean + description: Retry webhooks until acknowledged + ackNotificationsOn: + type: boolean + description: Include message ACK / status events in webhooks + rawHooks: + type: boolean + description: Forward raw provider webhook payloads + mm_lite_enabled: + type: boolean + template_analytics_enabled: + type: boolean + cta_url_link_tracking_opted_out: + type: boolean + haveDisabledWebhooks: + type: boolean + description: > + `true` when at least one URL in `webhookUrl` is sticky + auto-disabled. + + Not "the channel is dead": other configured URLs still + receive events. + circuitState: + type: string + description: > + Compatibility scalar for **one** URL (first + sticky-disabled, else first + + configured). Prefer `webhookStatuses[].circuitState`. + enum: + - ok + - degraded + - open + - half_open + - disabled + reason: + type: string + description: Last qualifying fail reason of the summary URL + fails: + type: integer + description: > + Diagnostic only (summary URL). Qualifying failures in the + rolling + + ~10 minute breaker window — not lifetime. Prefer + `webhookStatuses`. + trips: + type: integer + description: > + Diagnostic only (summary URL). Failed recovery probes (≈15 + min → + + 1 h → 6 h). Sticky disable after 3. Not a blind unblock + timer. + disabledAt: + type: integer + description: Epoch milliseconds when the summary URL became sticky-disabled + firstFailAt: + type: integer + description: > + Diagnostic only (summary URL). Epoch ms of the first fail + in the + + current ~10 minute window (pairs with `fails`). + lastStatus: + type: integer + description: Last HTTP status from the summary URL, when known + nextProbeAt: + type: integer + description: Epoch milliseconds of the next half-open probe for the summary URL, + if any + disabledWebhooks: + type: array + description: Currently configured URLs that are sticky auto-disabled (subset of + `webhookUrl`) + items: + type: object + additionalProperties: true + webhookStatuses: + type: array + description: One entry per currently configured URL. Prefer this over top-level + `haveDisabledWebhooks` / `circuitState`. + items: + $ref: "#/components/schemas/WebhookUrlStatus" + additionalProperties: true + example: + webhookUrl: https://example.com/webhook + guaranteedHooks: false + ackNotificationsOn: true + rawHooks: false + haveDisabledWebhooks: false + circuitState: ok + disabledWebhooks: [] + webhookStatuses: + - url: https://example.com/webhook + circuitState: ok + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Channel + operationId: listSettings + post: + summary: Update channel settings + description: > + Update channel settings. Only send fields you want to change. + + + `webhookUrl` **replaces** the stored list (one URL string, or up to 5 + URLs as an array). + + Passing an empty value clears webhooks. Repeating the same URL(s) also + clears a + + sticky auto-disable. With several URLs, send the complete array to + re-enable one + + of them — omitting a URL removes it from the channel. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + webhookUrl: + oneOf: + - type: string + - type: array + items: + type: string + description: Replacement webhook URL, or array of URLs (max 5) + guaranteedHooks: + type: boolean + ackNotificationsOn: + type: boolean + rawHooks: + type: boolean + template_analytics_enabled: + type: boolean + cta_url_link_tracking_opted_out: + type: boolean + additionalProperties: true + example: + webhookUrl: https://example.com/webhook + ackNotificationsOn: true + responses: + "200": + description: Updated settings + content: + application/json: + schema: + type: object + additionalProperties: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Channel + operationId: createSettings + /commerce: + get: + summary: Get Commerce Settings + description: > + Returns catalog/cart commerce settings for the channel. + + + Fails with a provider error when the WABA has no catalog linked — that + is an + + API/account limitation, not an MCP schema bug. + + + - `is_catalog_visible` — show catalog storefront icon (`true`) or hide + it (`false`). + + - `is_cart_enabled` — enable cart (`true`) or disable it (`false`). + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Commerce settings + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + description: Catalog id + is_cart_enabled: + type: boolean + description: Whether the cart is enabled + is_catalog_visible: + type: boolean + description: Whether the catalog storefront icon is shown + additionalProperties: true + example: + - id: "789887292550821" + is_cart_enabled: true + is_catalog_visible: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Catalog + operationId: getCommerce + post: + summary: Set Commerce Settings + description: > + Update catalog/cart commerce settings via the `params` object. + + + - `params.is_catalog_visible` — show catalog storefront icon (`true`) or + hide it (`false`). + + - `params.is_cart_enabled` — enable cart (`true`) or disable it + (`false`). + + + Blocked when the channel subscription limit is exceeded. + + Requires a commerce-capable channel (Cloud Functions `/commerceWAV2`). + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - params + properties: + params: + type: object + required: + - is_cart_enabled + - is_catalog_visible + properties: + is_cart_enabled: + type: boolean + description: Enable cart + is_catalog_visible: + type: boolean + description: Show catalog storefront icon + additionalProperties: true + additionalProperties: true + example: + params: + is_cart_enabled: true + is_catalog_visible: true + responses: + "200": + description: Commerce updated + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + additionalProperties: true + example: + success: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Catalog + operationId: createCommerce + /conversationalAutomation: + get: + summary: Get conversational automation settings + description: > + Get WhatsApp conversational components for the channel (welcome message, + + ice-breaker prompts, and slash commands). + + + Proxies Meta/360dialog `GET /conversational_automation`. + + + When `enable_welcome_message` is true and a user opens chat for the + first + + time, Meta delivers a webhook message with `type: request_welcome`. The + + inbound formatter exposes that as `type: "request_welcome"` and + + `meta.request_welcome: true` so your webhook can send a custom welcome + reply. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Conversational automation settings + content: + application/json: + schema: + $ref: "#/components/schemas/ConversationalAutomation" + example: + enable_welcome_message: true + prompts: + - How can I help? + commands: + - command_name: help + command_description: Get support options + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Channel + operationId: getConversationalAutomation + post: + summary: Set conversational automation settings + description: | + Update WhatsApp conversational components. + + Allowed body fields (others are ignored): + - `enable_welcome_message` (boolean) + - `prompts` (string[], max 4, each ≤ 80 chars) + - `commands` (`{ command_name, command_description }[]`) + + Proxies Meta/360dialog `POST /conversational_automation`. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ConversationalAutomation" + example: + enable_welcome_message: true + prompts: + - How can I help? + commands: + - command_name: help + command_description: Get support options + responses: + "200": + description: > + Updated settings from WABA, or legacy response.error wrapper on + upstream failure. + content: + application/json: + schema: + type: object + additionalProperties: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Channel + operationId: setConversationalAutomation + /webhook: + get: + summary: Get webhook URL + description: > + Return the webhook URL(s) currently configured for this channel, plus + delivery + + status for each URL. + + + `webhookUrl` is a string when a single URL is set, an array of strings + when + + multiple URLs are configured, or `null` if none is set. + + + **Prefer `webhookStatuses`.** The circuit breaker is per URL, not per + channel. + + If 1 of 4 URLs is sticky-disabled, that URL is skipped; the other three + still + + receive events. Send/receive are not affected. + + + Channel-level `haveDisabledWebhooks`, `circuitState`, `reason`, `fails`, + + `trips`, and timestamps are a compatibility summary of **one** URL (the + first + + sticky-disabled URL, else the first configured URL). They are not "the + channel + + is dead". + + + - `haveDisabledWebhooks` — `true` when **at least one** URL in + `webhookUrl` is + sticky auto-disabled. Live URLs still get events. + - `circuitState` — compressed scalar for that same primary URL: + `ok` | `degraded` | `open` | `half_open` | `disabled`. + - `disabledWebhooks` — currently configured URLs that are + sticky-disabled + (subset of `webhookUrl`). + - `webhookStatuses` — one row per currently configured URL. Use this + array + to see which endpoints are live vs skipped. + - `fails` / `trips` / `firstFailAt` — **diagnostics for support**, not + product + UI. `fails` = failures in a rolling ~10 minute window (not lifetime). + `trips` = failed recovery probes on the 15 min → 1 h → 6 h schedule (sticky + disable after 3). Clients can ignore these and use `webhookStatuses` + + `disabledWebhooks` only. + + Sticky disable skips delivery; it does not unconfigure the URL (it stays + in + + `webhookUrl`). Re-enable with `POST /webhook` or `POST /settings`. Those + + endpoints **replace** the whole list — with multiple URLs, send the full + array. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Current webhook URL and per-URL circuit status + content: + application/json: + schema: + type: object + properties: + webhookUrl: + oneOf: + - type: string + format: uri + - type: array + items: + type: string + format: uri + - type: "null" + description: Configured webhook URL, array of URLs, or null when none is set + haveDisabledWebhooks: + type: boolean + description: > + `true` when at least one URL in `webhookUrl` is sticky + auto-disabled. + + Not "the channel is dead": other configured URLs still + receive events. + + Send/receive are unaffected. + circuitState: + type: string + description: > + Compatibility scalar for **one** URL (first + sticky-disabled, else first + + configured). Not the channel as a whole. Prefer + `webhookStatuses[].circuitState`. + enum: + - ok + - degraded + - open + - half_open + - disabled + reason: + type: string + description: Last qualifying fail reason of the summary URL (same URL as + `circuitState`) + fails: + type: integer + description: > + Diagnostic only (summary URL). Qualifying failures in the + rolling + + ~10 minute breaker window — not lifetime and not customer + retries. + + Prefer `webhookStatuses` / `disabledWebhooks`. + trips: + type: integer + description: > + Diagnostic only (summary URL). Failed recovery probes (≈15 + min → + + 1 h → 6 h). Sticky disable after 3. Not a blind unblock + timer. + disabledAt: + type: integer + description: Epoch milliseconds when the summary URL became sticky-disabled + firstFailAt: + type: integer + description: > + Diagnostic only (summary URL). Epoch ms of the first fail + in the + + current ~10 minute window (pairs with `fails`). + lastStatus: + type: integer + description: Last HTTP status from the summary URL, when known + nextProbeAt: + type: integer + description: Epoch milliseconds of the next half-open probe for the summary URL, + if any + disabledWebhooks: + type: array + description: > + Currently configured URLs that are sticky auto-disabled + (subset of `webhookUrl`). + + Does not include leftover URLs that are no longer + configured. + items: + type: object + additionalProperties: true + webhookStatuses: + type: array + description: > + One entry per currently configured URL. Prefer this over + top-level + + `haveDisabledWebhooks` / `circuitState` when more than one + webhook is set. + items: + $ref: "#/components/schemas/WebhookUrlStatus" + additionalProperties: true + examples: + healthy: + summary: Single URL delivering + value: + webhookUrl: https://example.com/webhook + haveDisabledWebhooks: false + circuitState: ok + disabledWebhooks: [] + webhookStatuses: + - url: https://example.com/webhook + circuitState: ok + mixed: + summary: One of several URLs sticky-disabled + value: + webhookUrl: + - https://live.example.com/webhook + - https://down.example.com/webhook + haveDisabledWebhooks: true + circuitState: disabled + reason: http_404 + fails: 1 + trips: 3 + disabledWebhooks: + - url: https://down.example.com/webhook + reason: http_404 + state: disabled + fails: 1 + trips: 3 + webhookStatuses: + - url: https://live.example.com/webhook + circuitState: ok + - url: https://down.example.com/webhook + circuitState: disabled + reason: http_404 + fails: 1 + trips: 3 + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Webhooks + operationId: getWebhook + post: + summary: Set webhook URL + description: > + Set the client webhook URL that receives inbound events for this + channel. + + + **Replaces** the stored webhook list (it does not append). To deliver + the same + + events to several endpoints, pass an array in `webhookUrl` (max 5) or + use + + `POST /settings`. With several URLs already configured, posting only the + + disabled URL **drops the others** — send the complete array to re-enable + one + + of many. + + + Incoming payloads include messages, delivery ACKs, group updates, + coexistence + + echoes, and calling events. WhatsApp Calling payloads (`type: "calls"`) + are + + documented under the **Calling** tag. Call permission replies arrive on + the + + normal messages path (`call_permission_reply`). + + + Repeating `POST /webhook` with the same URL(s) clears an auto-disable + (self-serve + + re-enable). Send/receive are not affected by webhook auto-disable. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - webhookUrl + properties: + webhookUrl: + oneOf: + - type: string + format: uri + - type: array + items: + type: string + format: uri + maxItems: 5 + description: > + HTTP or HTTPS URL, or array of URLs (max 5). Replaces the + stored list. + + Empty values are rejected — clear webhooks with `POST + /settings`. + additionalProperties: true + examples: + single: + summary: Single URL + value: + webhookUrl: https://example.com/webhook + multiple: + summary: Several URLs (replaces the list) + value: + webhookUrl: + - https://example.com/webhook + - https://backup.example.com/webhook + responses: + "200": + description: Webhook configured + content: + application/json: + schema: + type: object + properties: + set: + type: boolean + description: True when the stored webhook URL(s) match the request + webhookUrl: + oneOf: + - type: string + - type: array + items: + type: string + description: Currently stored webhook URL or list + additionalProperties: true + example: + set: true + webhookUrl: https://example.com/webhook + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Webhooks + operationId: setWebhook + /callingSettings: + get: + summary: Get calling settings + description: > + Return WhatsApp Calling API settings for this channel (beta). + + + Proxies upstream `GET /calling/settings`. + + + **Prerequisites** + + - Number must be eligible for Meta Calling (Cloud API; not COEX) + + - Trial / `subscriptionBlocked` channels receive **403** plain text + + - You need your own WebRTC or SIP stack; 1msg is a **signaling proxy** + only + and does **not** store call history or recordings + + See the **Calling** tag overview for inbound/outbound flows and + webhooks. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Calling settings (and possibly other Cloud API settings Meta returns) + content: + application/json: + schema: + $ref: "#/components/schemas/CallingSettings" + example: + calling: + status: ENABLED + call_icon_visibility: DEFAULT + callback_permission_status: ENABLED + sip: + status: DISABLED + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Calling + operationId: getCallingSettings + post: + summary: Update calling settings + description: > + Enable, disable, or update WhatsApp Calling settings (beta). + + + Proxies upstream `POST /calling/settings`. + + Body is forwarded as-is (1msg does not validate fields). + + + **Common fields under `calling`** + + - `status` (`ENABLED` | `DISABLED`) — required to turn calling on/off + + - `call_icon_visibility` (`DEFAULT` | `DISABLE_ALL`) — optional + + - `callback_permission_status` (`ENABLED` | `DISABLED`) — optional; + when enabled, inbound user calls grant callback permission + - `call_hours` — optional hours / timezone object + + - `sip` — optional SIP trunk; when SIP is ENABLED, Graph call actions + and + calling webhooks are not used + - `srtp_key_exchange_protocol` (`DTLS` | `SDES`) — SDES only with SIP + + - `video.status` — optional + + + Meta may accept only one feature group per request — prefer focused + updates + + (e.g. enable status first, then SIP). + + + Trial / `subscriptionBlocked` → **403** plain text. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CallingSettings" + examples: + enable: + summary: Enable calling + value: + calling: + status: ENABLED + call_icon_visibility: DEFAULT + callback_permission_status: ENABLED + disable: + summary: Disable calling + value: + calling: + status: DISABLED + enableSip: + summary: Enable SIP (disables Graph call webhooks) + value: + calling: + status: ENABLED + sip: + status: ENABLED + servers: + - hostname: sip.example.com + port: 5061 + responses: + "200": + description: | + Usually `{ "success": true }` from upstream, or legacy + `{ "result": "success" }` / `{ "response": { "error": "..." } }`. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + result: + type: string + example: success + response: + type: object + properties: + error: + type: string + additionalProperties: true + additionalProperties: true + example: + success: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Calling + operationId: updateCallingSettings + /initiateCall: + post: + summary: Call action (connect / pre_accept / accept / reject / terminate) + description: > + Perform a WhatsApp Calling action (beta). + + + Proxies upstream `POST /calling/calls`. Despite the historical path + + name `/initiateCall`, this endpoint handles **all** call actions: + + + | action | Use | Required | + + |--------|-----|----------| + + | `connect` | Outbound business → user | `to` + `session` (`sdp_type: + offer`) | + + | `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + + `session` (`sdp_type: answer`) | + + | `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) + | + + | `reject` | Decline inbound | `call_id` | + + | `terminate` | Hang up | `call_id` | + + + **SDP / media (critical)** + + - `accept` / `pre_accept` require a **WebRTC-generated SDP answer**. + + - Do **not** send Meta's offer SDP back as the answer. + + - Postman (or curl) alone **cannot** establish real media — you need a + WebRTC or SIP stack. 1msg only proxies signaling. + + Answer within ~**30–60 seconds** of an inbound `connect` webhook or Meta + + terminates as unanswered. Common Meta errors include Calling not enabled + + (`138000`), no permission (`138006`), SDP validation failures. + + + **Outbound** requires a prior Call Permission Request (CPR) acceptance. + + See the **Calling** tag overview for the full outbound flow and CPR + limits. + + + Trial / `subscriptionBlocked` → **403** plain text. + + Upstream failures often return HTTP 200 with `{ "response": { "error": + "" } }`. + + `Invalid API KEY` is used only when Meta/360 body actually indicates an + + invalid API key or token. Other upstream 401s (notably call permission + + `138006`) pass through `error.message` / `error_user_msg`. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InitiateCallRequest" + examples: + outboundConnect: + summary: Outbound connect (business SDP offer) + value: + messaging_product: whatsapp + to: "12185552828" + action: connect + biz_opaque_callback_data: order-123 + session: + sdp_type: offer + sdp: "[REPLACE_WITH_WEBRTC_SDP]" + preAccept: + summary: Inbound pre_accept (WebRTC answer SDP) + value: + messaging_product: whatsapp + call_id: wacid.ABGGFjFVU2AfAgo6V-Hc5eCgK5Gh + action: pre_accept + session: + sdp_type: answer + sdp: "[REPLACE_WITH_WEBRTC_SDP]" + accept: + summary: Inbound accept (WebRTC answer SDP) + value: + messaging_product: whatsapp + call_id: wacid.ABGGFjFVU2AfAgo6V-Hc5eCgK5Gh + action: accept + session: + sdp_type: answer + sdp: "[REPLACE_WITH_WEBRTC_SDP]" + reject: + summary: Reject inbound call + value: + messaging_product: whatsapp + call_id: wacid.ABGGFjFVU2AfAgo6V-Hc5eCgK5Gh + action: reject + terminate: + summary: Terminate active call + value: + messaging_product: whatsapp + call_id: wacid.ABGGFjFVU2AfAgo6V-Hc5eCgK5Gh + action: terminate + responses: + "200": + description: Call action result from upstream, or legacy error wrapper + content: + application/json: + schema: + $ref: "#/components/schemas/InitiateCallResponse" + examples: + success: + summary: Accept / pre_accept / reject / terminate success + value: + messaging_product: whatsapp + success: true + connectSuccess: + summary: Outbound connect success (includes call id) + value: + messaging_product: whatsapp + calls: + - id: wacid.HBgLMTIxODU1NTI4MjgVAgARGCAyODRQIAFRoA + upstreamError: + summary: Upstream error wrapped by 1msg + value: + response: + error: Calling not enabled + callPermissionDenied: + summary: Meta 138006 (upstream 401, not Invalid API KEY) + value: + response: + error: No approved call permission from the recipient + invalidApiKey: + summary: Real invalid D360/Meta token + value: + response: + error: Invalid API KEY + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Calling + operationId: initiateCall + /groups: + post: + summary: Create Group + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - groupName + properties: + groupName: + type: string + description: Group subject / name + description: + type: string + description: Optional group description + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + error: + description: Error details returned by the provider. May be a string or a JSON + object. + additionalProperties: true + tags: + - Groups + operationId: createGroups + get: + summary: Get Groups List + description: List WhatsApp groups for this channel. Paginate with `limit`, + `before`, and `after`. + parameters: + - $ref: "#/components/parameters/token" + - name: limit + in: query + schema: + type: integer + description: Max groups to return + - name: before + in: query + schema: + type: string + description: Cursor for the previous page + - name: after + in: query + schema: + type: string + description: Cursor for the next page + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Groups + operationId: listGroups + /groups/{groupId}: + get: + summary: Get Group Info + parameters: + - $ref: "#/components/parameters/token" + - name: fields + in: query + schema: + type: string + description: Comma-separated group fields to return + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Groups + operationId: getGroupsGroupId + parameters: + - name: groupId + in: path + required: true + schema: + type: string + example: "{{createdGroupId}}" + post: + summary: Update Group Info + description: | + Update group subject, description, and/or profile picture. + Path `groupId` is the WABA group id (without `@g.us`). + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + subject: + type: string + description: New group subject / name + description: + type: string + description: New group description + profile_picture_file: + type: string + description: Image URL or base64 data URI for the group profile picture + example: + subject: Support team + description: Customer support + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Groups + operationId: createGroupsGroupId + delete: + summary: Delete Group + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + response: + type: object + additionalProperties: true + additionalProperties: true + tags: + - Groups + operationId: deleteGroupsGroupId + /groups/{groupId}/inviteLink: + get: + summary: Get Invite Link + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Groups + operationId: getGroupsGroupIdInvitelink + parameters: + - name: groupId + in: path + required: true + schema: + type: string + example: "{{createdGroupId}}" + post: + summary: Reset Invite Link + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Groups + operationId: createGroupsGroupIdInvitelink + /whatsapp_business_encryption: + get: + summary: Get business encryption public key + description: > + Retrieve the WhatsApp business public key and signature status for this + channel's + + phone number. Required before publishing or sending Flows that use data + encryption. + parameters: + - $ref: "#/components/parameters/token" + responses: + "200": + description: Business encryption key status + content: + application/json: + schema: + type: object + properties: + business_public_key: + type: string + description: Stored 2048-bit RSA public key (PEM) + business_public_key_signature_status: + type: string + enum: + - VALID + - MISMATCH + data: + type: array + items: + type: object + additionalProperties: true + additionalProperties: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Flows + operationId: getWhatsappBusinessEncryption + post: + summary: Set business encryption public key + description: > + Upload and sign a 2048-bit RSA business public key (PEM) for this + channel's phone number. + + Meta requires a signed key before Flow publish/send. Only one active key + per number; + + a new upload replaces the previous key. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - business_public_key + properties: + business_public_key: + type: string + description: 2048-bit RSA public key in PEM format + example: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... + -----END PUBLIC KEY----- + responses: + "200": + description: Public key uploaded and signed + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + additionalProperties: true + "400": + description: Missing or invalid PEM key + content: + application/json: + schema: + type: object + properties: + error: + type: string + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Flows + operationId: setWhatsappBusinessEncryption + /flows: + post: + summary: Create Flow + description: > + Create a WhatsApp Flow (`name` + `categories` required). + + Currently returns HTTP 501 until the Meta Graph Flows rewrite is + available. + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + - categories + properties: + name: + type: string + description: Flow name + categories: + type: array + items: + type: string + enum: + - SIGN_UP + - SIGN_IN + - APPOINTMENT_BOOKING + - LEAD_GENERATION + - CONTACT_US + - CUSTOMER_SUPPORT + - SURVEY + - OTHER + description: One or more Flow categories + endpointUri: + type: string + description: HTTPS endpoint for the Flow data channel + publish: + type: boolean + description: Publish immediately after create + flowJson: + type: object + additionalProperties: true + description: Optional Flow JSON document + example: + name: lead_form + categories: + - LEAD_GENERATION + publish: false + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + error: + description: Error details returned by the provider. May be a string or a JSON + object. + additionalProperties: true + "501": + description: WhatsApp Flows are not available on this channel yet + tags: + - Flows + operationId: createFlows + get: + summary: List Flows + description: > + List WhatsApp Flows for the WABA. + + Currently returns HTTP 501 until the Meta Graph Flows rewrite is + available. + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + - name: fields + in: query + schema: + type: string + description: Comma-separated Flow fields to return + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + additionalProperties: true + paging: + type: object + additionalProperties: true + count: + type: integer + total: + type: integer + error: + description: Error details returned by the provider. May be a string or a JSON + object. + additionalProperties: true + "501": + description: WhatsApp Flows are not available on this channel yet + tags: + - Flows + operationId: listFlows + /flows/{flowId}: + get: + summary: Get Flow Details + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + error: + description: Error details returned by the provider. May be a string or a JSON + object. + additionalProperties: true + tags: + - Flows + operationId: getFlowsFlowId + parameters: + - $ref: "#/components/parameters/flowId" + example: "{{flowId}}" + delete: + summary: Delete Flow + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + error: + description: Error details returned by the provider. May be a string or a JSON + object. + additionalProperties: true + tags: + - Flows + operationId: deleteFlowsFlowId + /flows/{flowId}/metadata: + patch: + summary: Update Flow Metadata + description: > + Update Flow `name`, `categories`, and/or `endpointUri`. + + Currently returns HTTP 501 until the Meta Graph Flows rewrite is + available. + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + categories: + type: array + items: + type: string + enum: + - SIGN_UP + - SIGN_IN + - APPOINTMENT_BOOKING + - LEAD_GENERATION + - CONTACT_US + - CUSTOMER_SUPPORT + - SURVEY + - OTHER + endpointUri: + type: string + description: HTTPS endpoint for the Flow data channel + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + "501": + description: WhatsApp Flows are not available on this channel yet + tags: + - Flows + operationId: patchFlowsFlowIdMetadata + parameters: + - $ref: "#/components/parameters/flowId" + example: "{{flowId}}" + /flows/{flowId}/assets: + patch: + summary: Update Flow Structure + description: > + Replace the Flow JSON document (`flowJson`). + + Currently returns HTTP 501 until the Meta Graph Flows rewrite is + available. + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - flowJson + properties: + flowJson: + type: object + additionalProperties: true + description: WhatsApp Flow JSON document + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + validationErrors: + type: array + items: + type: object + additionalProperties: true + error: + description: Error details returned by the provider. May be a string or a JSON + object. + additionalProperties: true + "501": + description: WhatsApp Flows are not available on this channel yet + tags: + - Flows + operationId: patchFlowsFlowIdAssets + parameters: + - $ref: "#/components/parameters/flowId" + example: "{{flowId}}" + /flows/{flowId}/preview: + get: + summary: Preview Flow + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Flows + operationId: getFlowsFlowIdPreview + parameters: + - $ref: "#/components/parameters/flowId" + example: "{{flowId}}" + /flows/{flowId}/publish: + post: + summary: Publish Flow + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Flows + operationId: createFlowsFlowIdPublish + parameters: + - $ref: "#/components/parameters/flowId" + example: "{{flowId}}" + /flows/{flowId}/deprecate: + post: + summary: Deprecate Flow + parameters: + - $ref: "#/components/parameters/token" + - name: wabaAccountId + in: query + schema: + type: string + example: "{{wabaAccountId}}" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: true + tags: + - Flows + operationId: createFlowsFlowIdDeprecate + parameters: + - $ref: "#/components/parameters/flowId" + example: "{{flowId}}" + /templates: + get: + summary: Get templates list + description: List message templates. Paginate with `limit` / `offset`; sort by + `id`, `name`, or `status`. + parameters: + - $ref: "#/components/parameters/token" + - name: limit + in: query + schema: + type: integer + - name: offset + in: query + schema: + type: integer + - name: sort + in: query + schema: + type: string + enum: + - id + - name + - status + description: Sort field (`id`, `name`, or `status`) + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + total: + type: integer + templates: + type: array + items: + type: object + additionalProperties: true + error: + type: object + additionalProperties: true + additionalProperties: true + tags: + - Templates + operationId: listTemplates + /addTemplate: + post: + summary: Create message template + description: > + Create a WABA message template (`name`, `category`, `language`, + `components`). + + After create, Meta reviews the template; list it with `GET /templates`. + parameters: + - $ref: "#/components/parameters/token" + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Template name + example: hello_world + category: + type: string + description: MARKETING, UTILITY, or AUTHENTICATION + example: UTILITY + language: + type: string + description: Template language code (e.g. en_US) + example: en_US + components: + type: array + items: + type: object + additionalProperties: true + description: Template components (HEADER, BODY, FOOTER, BUTTONS, CAROUSEL) + additionalProperties: true + example: + name: hello_world + category: UTILITY + language: en_US + components: + - type: BODY + text: Hello {{1}} + responses: + "200": + description: Template created. Provider payload is wrapped in `response`. + content: + application/json: + schema: + type: object + properties: + response: + type: object + description: Upstream create-template payload + additionalProperties: true + additionalProperties: true + example: + response: + id: "123456789" + status: PENDING + category: MARKETING + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Templates + operationId: addTemplate + /removeTemplate: + post: + summary: Remove message template + description: Delete a message template by `name` (all language versions of that name). + parameters: + - $ref: "#/components/parameters/token" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + description: Template name to delete + example: + name: hello_world + responses: + "200": + description: Template removed + content: + application/json: + schema: + type: object + additionalProperties: true + "401": + $ref: "#/components/responses/InvalidToken" + "500": + $ref: "#/components/responses/InternalError" + tags: + - Templates + operationId: removeTemplate +components: + parameters: + flowId: + name: flowId + in: path + required: true + schema: + type: string + description: Flow ID + token: + name: token + in: query + required: true + schema: + type: string + description: JWT token or API key for authorization + date: + name: date + in: query + required: false + schema: + type: string + format: date + description: Filter by date (YYYY-MM-DD) + groupId: + name: groupId + in: path + required: true + schema: + type: string + description: Group ID (phone number with country code) + schemas: + ErrorResponse: + type: object + properties: + error: + type: string + description: Error message + description: Standard error response + MessageSentResponse: + type: object + properties: + sent: + type: boolean + description: Whether the message was successfully queued for sending + id: + type: string + description: Message ID + message: + type: string + description: Status message + description: + type: string + description: Detailed description + additionalProperties: true + description: Response for successfully sent message + SuccessResponse: + type: object + properties: + result: + type: string + description: Operation result + additionalProperties: true + description: Generic success response + ProfileInfo: + type: object + properties: + about: + type: string + description: Profile's About section + address: + type: string + description: Address of the business + description: + type: string + description: Description of the business + email: + type: string + description: Business email + phone: + type: string + description: Linked phone number + vertical: + type: string + enum: + - Automotive + - Beauty, Spa and Salon + - Clothing and Apparel + - Education + - Entertainment + - Event Planning and Service + - Finance and Banking + - Food and Grocery + - Public Service + - Hotel and Lodging + - Medical and Health + - Non-profit + - Professional Services + - Shopping and Retail + - Travel and Transportation + - Restaurant + - Other + description: Industry of the business + photo: + type: string + description: Profile photo URL + websites: + type: array + items: + type: string + additionalProperties: true + description: WhatsApp Business profile information + ConversationalAutomation: + type: object + properties: + enable_welcome_message: + type: boolean + description: Enable the automatic welcome message for new conversations + prompts: + type: array + maxItems: 4 + description: Ice-breaker prompt strings (max 4, each ≤ 80 characters) + items: + type: string + maxLength: 80 + commands: + type: array + maxItems: 30 + description: Slash commands shown in WhatsApp + items: + type: object + required: + - command_name + - command_description + properties: + command_name: + type: string + maxLength: 32 + command_description: + type: string + maxLength: 256 + additionalProperties: true + additionalProperties: true + CallingSettings: + type: object + description: > + Calling settings payload for GET/POST `/callingSettings`. + + Proxies upstream `GET|POST /calling/settings`. + + Extra Meta fields may appear on GET responses; document core fields + below. + properties: + calling: + type: object + description: Calling feature configuration for the business phone number + properties: + status: + type: string + description: Enable or disable Calling API on this number + enum: + - ENABLED + - DISABLED + example: ENABLED + call_icon_visibility: + type: string + description: Controls call icon visibility in the WhatsApp client + enum: + - DEFAULT + - DISABLE_ALL + example: DEFAULT + callback_permission_status: + type: string + description: > + When ENABLED, a user who calls your business automatically + grants + + call permission for business-initiated callbacks (subject to + Meta rules). + enum: + - ENABLED + - DISABLED + example: ENABLED + srtp_key_exchange_protocol: + type: string + description: | + SRTP key exchange. DTLS is default/recommended. + SDES is only valid when SIP signaling is enabled. + enum: + - DTLS + - SDES + example: DTLS + call_hours: + type: object + description: Optional call hours / timezone configuration (Meta shape) + additionalProperties: true + properties: + status: + type: string + example: ENABLED + timezone: + type: string + example: America/Los_Angeles + day_of_week_start: + type: string + example: MONDAY + sip: + type: object + description: > + SIP trunk settings. When SIP is ENABLED, Graph call actions and + + calling webhooks are not used — Meta dials your SIP server + directly. + properties: + status: + type: string + enum: + - ENABLED + - DISABLED + example: DISABLED + servers: + type: array + items: + type: object + properties: + hostname: + type: string + example: sip.example.com + port: + type: integer + example: 5061 + request_uri_user_params: + type: object + additionalProperties: + type: string + sip_user_password: + type: string + description: Present on GET when SIP credentials are returned by Meta + password: + type: string + description: Alternate password field name from some Meta responses + app_id: + type: string + additionalProperties: true + additionalProperties: true + video: + type: object + description: Video calling toggle when supported + properties: + status: + type: string + enum: + - ENABLED + - DISABLED + additionalProperties: true + audio: + type: object + description: Audio settings (typically response-only) + properties: + status: + type: string + additionalProperties: true + restrictions: + type: object + description: Calling restrictions (response-only) + additionalProperties: true + ip_addresses: + type: object + description: Meta media IP ranges (response-only) + additionalProperties: true + call_icons: + type: object + description: Call icon country restrictions (response-only) + additionalProperties: true + additionalProperties: true + additionalProperties: true + InitiateCallRequest: + type: object + description: > + Call action payload for `POST /initiateCall` (proxies upstream `POST + /calling/calls`). + + + Despite the path name, this endpoint handles **all** call actions — not + only + + outbound connect. + + + ## Required fields by `action` + + + | action | Required | `session` | + + |--------|----------|-----------| + + | `connect` (outbound) | `messaging_product`, `to`, `action` | + `sdp_type: offer` + business WebRTC SDP | + + | `pre_accept` | `messaging_product`, `call_id`, `action` | `sdp_type: + answer` (WebRTC-generated) | + + | `accept` | `messaging_product`, `call_id`, `action` | `sdp_type: + answer` (WebRTC-generated) | + + | `reject` | `messaging_product`, `call_id`, `action` | none | + + | `terminate` | `messaging_product`, `call_id`, `action` | none | + + + **Critical:** For `pre_accept` / `accept`, `session.sdp` must be a + + **WebRTC-generated SDP answer**. Do not echo Meta's offer SDP back. + + Postman alone cannot establish real media — you need a WebRTC (or SIP) + stack. + required: + - messaging_product + - action + properties: + messaging_product: + type: string + description: Must be `whatsapp` + enum: + - whatsapp + example: whatsapp + action: + type: string + description: Call control action + enum: + - connect + - pre_accept + - accept + - reject + - terminate + example: connect + call_id: + type: string + description: > + WhatsApp call id from the inbound/outbound calls webhook + (`calls[].id`). + + Required for `pre_accept`, `accept`, `reject`, `terminate`. + example: wacid.ABGGFjFVU2AfAgo6V-Hc5eCgK5Gh + to: + type: string + description: | + Recipient WhatsApp user phone (digits, country code, no +). + Required for outbound `connect`. + example: "12185552828" + biz_opaque_callback_data: + type: string + description: Optional opaque string echoed on later call webhooks for correlation + maxLength: 512 + example: order-123-callback + session: + type: object + description: WebRTC SDP session (required for connect / pre_accept / accept) + required: + - sdp_type + - sdp + properties: + sdp_type: + type: string + description: offer for outbound connect; answer for pre_accept/accept + enum: + - offer + - answer + example: offer + sdp: + type: string + description: > + Full SDP (RFC 8866). Replace placeholders with SDP from your + WebRTC stack. + + Must negotiate ICE, DTLS-SRTP, and OPUS for WhatsApp media. + example: "[REPLACE_WITH_WEBRTC_SDP]" + additionalProperties: true + additionalProperties: true + InitiateCallResponse: + type: object + description: > + Successful call action response from upstream, returned as-is. + + On empty upstream body, 1msg may return `{ "result": "success" }`. + + Upstream failures are often wrapped as HTTP 200 `{ "response": { + "error": "..." } }`. + properties: + messaging_product: + type: string + example: whatsapp + success: + type: boolean + example: true + calls: + type: array + description: Present on outbound `connect` — contains the new call id + items: + type: object + properties: + id: + type: string + example: wacid.HBgLMTIxODU1NTI4MjgVAgARGCAyODRQIAFRoA + additionalProperties: true + result: + type: string + description: Legacy success marker when upstream returns an empty body + example: success + response: + type: object + description: Present when 1msg wraps an upstream error + properties: + error: + type: string + additionalProperties: true + additionalProperties: true + WebhookUrlStatus: + type: object + description: | + Circuit / auto-disable status for one configured webhook URL. + The breaker is per URL (`instanceId` + URL hash), not per channel. + required: + - url + - circuitState + properties: + url: + type: string + format: uri + description: This webhook URL (same string as in `webhookUrl`) + circuitState: + type: string + description: > + Delivery state for this URL only. `ok` delivering; `degraded` recent + qualifying fails (still delivering, warning email on the 2nd fail); + `open` skipping most deliveries pending a probe; `half_open` probe + in flight (resume only on HTTP 2xx); `disabled` sticky auto-disabled + until self-serve re-enable (`POST /webhook` / `POST /settings` with + this URL) or a successful probe before sticky disable. + enum: + - ok + - degraded + - open + - half_open + - disabled + reason: + type: string + description: Last qualifying fail reason, e.g. `http_404` or `timeout_or_network` + fails: + type: integer + description: > + Diagnostic only — safe for clients to ignore. Number of qualifying + delivery + + failures in the rolling ~10 minute breaker window (not lifetime, not + "how many + + times the customer retried"). Prefer `circuitState` / + `disabledWebhooks`. + trips: + type: integer + description: > + Diagnostic only — safe for clients to ignore. How many recovery + probes already + + failed for this URL. Probes run on a schedule (≈15 min → 1 h → 6 h); + sticky + + disable after 3 failed probes. The 6 h step is the last probe, not a + blind + + "unblock in 6 hours" timer. + disabledAt: + type: integer + description: Epoch milliseconds when this URL became sticky-disabled + firstFailAt: + type: integer + description: > + Diagnostic only. Epoch ms of the first fail in the current ~10 + minute window + + (pairs with `fails`; not a customer-facing period). + lastStatus: + type: integer + description: Last HTTP status from this URL, when known + nextProbeAt: + type: integer + description: Epoch milliseconds of the next half-open probe, if any + additionalProperties: true + SendMessageRequest: + type: object + required: + - body + properties: + body: + type: string + description: Message text, UTF-8 or UTF-16 string with emoji + minLength: 1 + maxLength: 4096 + example: Hello World 🌍 + quotedMsgId: + type: string + description: Message ID to quote/reply to (Cloud API wamid) + pattern: ^wamid\..+$ + example: wamid.HBgNMTIwMjA3MjEzNjkVAgARGBI5RTBCNUY0QUE2RjZBQzhGNDkA + chatId: + type: string + description: "Recipient chat ID: phone@c.us, group@g.us, or BSUID@lid" + example: 12020721369@c.us + phone: + type: integer + description: Phone number starting with country code (alternative to chatId) + minimum: 1000000000 + maximum: 999999999999999 + example: 12020721369 + additionalProperties: false + description: Request body for sending a text message + SendFileRequest: + type: object + properties: + body: + type: string + description: | + File source (required unless mediaId is set): + - HTTP/HTTPS URL + - Base64 data URI + filename: + type: string + description: File name with extension (required with body, not with mediaId) + mediaId: + type: string + description: WABA media id from uploadMedia (alternative to body) + mediaType: + type: string + enum: + - image + - video + - audio + - document + description: Required when using mediaId + voice: + type: boolean + description: Native WhatsApp voice note (audio/ogg only) + caption: + type: string + maxLength: 1024 + quotedMsgId: + type: string + chatId: + type: string + phone: + type: integer + additionalProperties: true + description: Send file by URL/base64 or by mediaId + responses: + InvalidToken: + description: Invalid or missing authentication token + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + InternalError: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + MessageSent: + description: Message sent successfully + content: + application/json: + schema: + $ref: "#/components/schemas/MessageSentResponse" + BadRequest: + description: Invalid request parameters + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + TooManyRequests: + description: Rate limit exceeded + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + Success: + description: Operation successful + content: + application/json: + schema: + $ref: "#/components/schemas/SuccessResponse" + securitySchemes: + tokenAuth: + type: apiKey + in: query + name: token + description: JWT token or API key + requestBodies: {} +security: + - tokenAuth: [] diff --git a/quality/mcp/provenance.json b/quality/mcp/provenance.json new file mode 100644 index 0000000..76f419b --- /dev/null +++ b/quality/mcp/provenance.json @@ -0,0 +1,22 @@ +{ + "sourceRepository": "https://github.com/1msg/1msg-api", + "producerSha256": "c6bd3039334b1d4f36d531db1c97596af3467862327d11437c65c42b413df066", + "contractSha256": "b26aa068f67c892144f2f9f2d9d0ce7a39bb12ff8ad01fd52e68f950fcc2f18c", + "sdkVersion": "1.0.3", + "sdkSignatures": { + "CallingApi.ts": "129eb7db5b92150022f9da64dae6b85a7db267ae3478e1979f8d71583db458e0", + "CatalogApi.ts": "b85c2c5a0f61f6cfbfca720a1e0c03a5b9abf7b680cec6b420705329337f7fc8", + "ChannelApi.ts": "a969213513f911a04bbce46357591e9c46220d8b474e075303bfd54b3b6123ff", + "FlowsApi.ts": "c4834948439172c986483c8a3110f49a4d138fa262be8452dba34404b85d8c15", + "GroupsApi.ts": "ca771e833b3506b247bf9c1c32a194573d1ef72fe0ee1ca8e8b8ca6ffedb662b", + "MessagingApi.ts": "9d57d04fbf78687b94a5333922dc843b62f25a83bf779a34722fdd2588c646d2", + "ProfileApi.ts": "b11f725dea52f2afe672b2b6413346f529feb315d336d68712b2f50f5403d217", + "TemplatesApi.ts": "15043c2f7ef1952982681105cc83d4599222feeeb1029ef83e65692b72495639", + "UsersApi.ts": "65cbbf11185465c4e8a0a82faa0e62f61f681a449d78f73c1714b9fa1639a5b6", + "WebhooksApi.ts": "a64d716467e1643463f0d0c1a508938a0cd0ab4a16bd1e1bf0e8adb9a5df4208" + }, + "contractSourceCommit": "888ee40a635ab19dbff4c101b8d4329790c7254f", + "producerTask": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "producerSource": "1msg-api/gen/generate-mcp-tools.js with the reviewed 1MSG-156 numeric media ID compatibility patch", + "signatureProducer": "OpenAPI Generator 7.22.0 plus 1msg-api TypeScript compatibility transforms, same pinned public contract" +} diff --git a/quality/mcp/sdk-compatibility-exception.json b/quality/mcp/sdk-compatibility-exception.json new file mode 100644 index 0000000..c49ae24 --- /dev/null +++ b/quality/mcp/sdk-compatibility-exception.json @@ -0,0 +1,141 @@ +{ + "id": "published-sdk-1.0.3-signature-lag", + "owner": "1MSG SDK/MCP maintainers", + "issue": "https://plane.1msg.io/1msg/browse/1MSG-156/", + "expires": "2026-12-11", + "scope": "Parameter-list compatibility between the public-contract-generated SDK and the locked published SDK used by MCP. No assertion of full wire compatibility is made for these operations.", + "reason": "Existing MCP handlers target workspace SDK signatures but the locked published SDK still lacks these parameters. Release the reviewed TypeScript SDK and update MCP dependency, then remove this exception after real wire tests. Do not publish from the quality task.", + "findings": [ + { + "api": "ChannelApi", + "method": "createSettings", + "expectedParameters": ["token", "createSettingsRequest"], + "publishedParameters": null + }, + { + "api": "ChannelApi", + "method": "listSettings", + "expectedParameters": ["token"], + "publishedParameters": null + }, + { + "api": "FlowsApi", + "method": "createFlows", + "expectedParameters": ["token", "createFlowsRequest", "wabaAccountId?"], + "publishedParameters": ["token", "wabaAccountId?"] + }, + { + "api": "FlowsApi", + "method": "listFlows", + "expectedParameters": ["token", "wabaAccountId?", "fields?"], + "publishedParameters": ["token", "wabaAccountId?"] + }, + { + "api": "FlowsApi", + "method": "patchFlowsFlowIdAssets", + "expectedParameters": ["flowId", "token", "patchFlowsFlowIdAssetsRequest", "wabaAccountId?"], + "publishedParameters": ["flowId", "token", "wabaAccountId?"] + }, + { + "api": "FlowsApi", + "method": "patchFlowsFlowIdMetadata", + "expectedParameters": [ + "flowId", + "token", + "patchFlowsFlowIdMetadataRequest", + "wabaAccountId?" + ], + "publishedParameters": ["flowId", "token", "wabaAccountId?"] + }, + { + "api": "GroupsApi", + "method": "createGroups", + "expectedParameters": ["token", "createGroupsRequest"], + "publishedParameters": ["token"] + }, + { + "api": "GroupsApi", + "method": "createGroupsGroupId", + "expectedParameters": ["groupId", "token", "createGroupsGroupIdRequest"], + "publishedParameters": ["groupId", "token"] + }, + { + "api": "GroupsApi", + "method": "getGroupsGroupId", + "expectedParameters": ["groupId", "token", "fields?"], + "publishedParameters": ["groupId", "token"] + }, + { + "api": "GroupsApi", + "method": "listGroups", + "expectedParameters": ["token", "limit?", "before?", "after?"], + "publishedParameters": ["token"] + }, + { + "api": "MessagingApi", + "method": "createUploadMedia", + "expectedParameters": ["token", "createUploadMediaRequest"], + "publishedParameters": ["token", "requestBody?"] + }, + { + "api": "MessagingApi", + "method": "listMessages", + "expectedParameters": [ + "token", + "chatId?", + "limit?", + "last?", + "lastMessageNumber?", + "firstMessageNumber?", + "minTime?", + "maxTime?", + "msgId?" + ], + "publishedParameters": ["token"] + }, + { + "api": "MessagingApi", + "method": "sendCarousel", + "expectedParameters": [ + "token", + "body?", + "cards?", + "params?", + "quotedMsgId?", + "chatId?", + "phone?" + ], + "publishedParameters": ["token", "params", "body?", "quotedMsgId?", "chatId?", "phone?"] + }, + { + "api": "MessagingApi", + "method": "sendProduct", + "expectedParameters": ["token", "sendProductRequest"], + "publishedParameters": ["token", "requestBody?"] + }, + { + "api": "ProfileApi", + "method": "updateMe", + "expectedParameters": ["token", "updateMeRequest"], + "publishedParameters": ["token", "updateMeRequest?"] + }, + { + "api": "TemplatesApi", + "method": "listTemplates", + "expectedParameters": ["token", "limit?", "offset?", "sort?"], + "publishedParameters": ["token"] + }, + { + "api": "TemplatesApi", + "method": "removeTemplate", + "expectedParameters": ["token", "removeTemplateRequest"], + "publishedParameters": ["token", "requestBody?"] + }, + { + "api": "WebhooksApi", + "method": "setWebhook", + "expectedParameters": ["token", "setWebhookRequest"], + "publishedParameters": ["token", "getWebhook200Response?"] + } + ] +} diff --git a/quality/mcp/vendor/apis/CallingApi.ts b/quality/mcp/vendor/apis/CallingApi.ts new file mode 100644 index 0000000..d922e4b --- /dev/null +++ b/quality/mcp/vendor/apis/CallingApi.ts @@ -0,0 +1,328 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type CallingSettings, + CallingSettingsFromJSON, + CallingSettingsToJSON, +} from '../models/CallingSettings'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type InitiateCallRequest, + InitiateCallRequestFromJSON, + InitiateCallRequestToJSON, +} from '../models/InitiateCallRequest'; +import { + type InitiateCallResponse, + InitiateCallResponseFromJSON, + InitiateCallResponseToJSON, +} from '../models/InitiateCallResponse'; +import { + type UpdateCallingSettings200Response, + UpdateCallingSettings200ResponseFromJSON, + UpdateCallingSettings200ResponseToJSON, +} from '../models/UpdateCallingSettings200Response'; + +export interface GetCallingSettingsRequest { + token: string; +} + +export interface InitiateCallOperationRequest { + token: string; + initiateCallRequest: InitiateCallRequest; +} + +export interface UpdateCallingSettingsRequest { + token: string; + callingSettings: CallingSettings; +} + +/** + * CallingApi - interface + * + * @export + * @interface CallingApiInterface + */ +export interface CallingApiInterface { + /** + * Creates request options for getCallingSettings without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof CallingApiInterface + */ + getCallingSettingsRequestOpts(requestParameters: GetCallingSettingsRequest): Promise; + + /** + * Return WhatsApp Calling API settings for this channel (beta). Proxies upstream `GET /calling/settings`. **Prerequisites** - Number must be eligible for Meta Calling (Cloud API; not COEX) - Trial / `subscriptionBlocked` channels receive **403** plain text - You need your own WebRTC or SIP stack; 1msg is a **signaling proxy** only and does **not** store call history or recordings See the **Calling** tag overview for inbound/outbound flows and webhooks. + * @summary Get calling settings + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CallingApiInterface + */ + getCallingSettingsRaw(requestParameters: GetCallingSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Return WhatsApp Calling API settings for this channel (beta). Proxies upstream `GET /calling/settings`. **Prerequisites** - Number must be eligible for Meta Calling (Cloud API; not COEX) - Trial / `subscriptionBlocked` channels receive **403** plain text - You need your own WebRTC or SIP stack; 1msg is a **signaling proxy** only and does **not** store call history or recordings See the **Calling** tag overview for inbound/outbound flows and webhooks. + * Get calling settings + */ + getCallingSettings(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for initiateCall without sending the request + * @param {string} token JWT token or API key for authorization + * @param {InitiateCallRequest} initiateCallRequest + * @throws {RequiredError} + * @memberof CallingApiInterface + */ + initiateCallRequestOpts(requestParameters: InitiateCallOperationRequest): Promise; + + /** + * Perform a WhatsApp Calling action (beta). Proxies upstream `POST /calling/calls`. Despite the historical path name `/initiateCall`, this endpoint handles **all** call actions: | action | Use | Required | |--------|-----|----------| | `connect` | Outbound business → user | `to` + `session` (`sdp_type: offer`) | | `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + `session` (`sdp_type: answer`) | | `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) | | `reject` | Decline inbound | `call_id` | | `terminate` | Hang up | `call_id` | **SDP / media (critical)** - `accept` / `pre_accept` require a **WebRTC-generated SDP answer**. - Do **not** send Meta\'s offer SDP back as the answer. - Postman (or curl) alone **cannot** establish real media — you need a WebRTC or SIP stack. 1msg only proxies signaling. Answer within ~**30–60 seconds** of an inbound `connect` webhook or Meta terminates as unanswered. Common Meta errors include Calling not enabled (`138000`), no permission (`138006`), SDP validation failures. **Outbound** requires a prior Call Permission Request (CPR) acceptance. See the **Calling** tag overview for the full outbound flow and CPR limits. Trial / `subscriptionBlocked` → **403** plain text. Upstream failures often return HTTP 200 with `{ \"response\": { \"error\": \"\" } }`. `Invalid API KEY` is used only when Meta/360 body actually indicates an invalid API key or token. Other upstream 401s (notably call permission `138006`) pass through `error.message` / `error_user_msg`. + * @summary Call action (connect / pre_accept / accept / reject / terminate) + * @param {string} token JWT token or API key for authorization + * @param {InitiateCallRequest} initiateCallRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CallingApiInterface + */ + initiateCallRaw(requestParameters: InitiateCallOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Perform a WhatsApp Calling action (beta). Proxies upstream `POST /calling/calls`. Despite the historical path name `/initiateCall`, this endpoint handles **all** call actions: | action | Use | Required | |--------|-----|----------| | `connect` | Outbound business → user | `to` + `session` (`sdp_type: offer`) | | `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + `session` (`sdp_type: answer`) | | `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) | | `reject` | Decline inbound | `call_id` | | `terminate` | Hang up | `call_id` | **SDP / media (critical)** - `accept` / `pre_accept` require a **WebRTC-generated SDP answer**. - Do **not** send Meta\'s offer SDP back as the answer. - Postman (or curl) alone **cannot** establish real media — you need a WebRTC or SIP stack. 1msg only proxies signaling. Answer within ~**30–60 seconds** of an inbound `connect` webhook or Meta terminates as unanswered. Common Meta errors include Calling not enabled (`138000`), no permission (`138006`), SDP validation failures. **Outbound** requires a prior Call Permission Request (CPR) acceptance. See the **Calling** tag overview for the full outbound flow and CPR limits. Trial / `subscriptionBlocked` → **403** plain text. Upstream failures often return HTTP 200 with `{ \"response\": { \"error\": \"\" } }`. `Invalid API KEY` is used only when Meta/360 body actually indicates an invalid API key or token. Other upstream 401s (notably call permission `138006`) pass through `error.message` / `error_user_msg`. + * Call action (connect / pre_accept / accept / reject / terminate) + */ + initiateCall(token: string, initiateCallRequest: InitiateCallRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for updateCallingSettings without sending the request + * @param {string} token JWT token or API key for authorization + * @param {CallingSettings} callingSettings + * @throws {RequiredError} + * @memberof CallingApiInterface + */ + updateCallingSettingsRequestOpts(requestParameters: UpdateCallingSettingsRequest): Promise; + + /** + * Enable, disable, or update WhatsApp Calling settings (beta). Proxies upstream `POST /calling/settings`. Body is forwarded as-is (1msg does not validate fields). **Common fields under `calling`** - `status` (`ENABLED` | `DISABLED`) — required to turn calling on/off - `call_icon_visibility` (`DEFAULT` | `DISABLE_ALL`) — optional - `callback_permission_status` (`ENABLED` | `DISABLED`) — optional; when enabled, inbound user calls grant callback permission - `call_hours` — optional hours / timezone object - `sip` — optional SIP trunk; when SIP is ENABLED, Graph call actions and calling webhooks are not used - `srtp_key_exchange_protocol` (`DTLS` | `SDES`) — SDES only with SIP - `video.status` — optional Meta may accept only one feature group per request — prefer focused updates (e.g. enable status first, then SIP). Trial / `subscriptionBlocked` → **403** plain text. + * @summary Update calling settings + * @param {string} token JWT token or API key for authorization + * @param {CallingSettings} callingSettings + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CallingApiInterface + */ + updateCallingSettingsRaw(requestParameters: UpdateCallingSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Enable, disable, or update WhatsApp Calling settings (beta). Proxies upstream `POST /calling/settings`. Body is forwarded as-is (1msg does not validate fields). **Common fields under `calling`** - `status` (`ENABLED` | `DISABLED`) — required to turn calling on/off - `call_icon_visibility` (`DEFAULT` | `DISABLE_ALL`) — optional - `callback_permission_status` (`ENABLED` | `DISABLED`) — optional; when enabled, inbound user calls grant callback permission - `call_hours` — optional hours / timezone object - `sip` — optional SIP trunk; when SIP is ENABLED, Graph call actions and calling webhooks are not used - `srtp_key_exchange_protocol` (`DTLS` | `SDES`) — SDES only with SIP - `video.status` — optional Meta may accept only one feature group per request — prefer focused updates (e.g. enable status first, then SIP). Trial / `subscriptionBlocked` → **403** plain text. + * Update calling settings + */ + updateCallingSettings(token: string, callingSettings: CallingSettings, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + +} + +/** + * + */ +export class CallingApi extends runtime.BaseAPI implements CallingApiInterface { + + /** + * Creates request options for getCallingSettings without sending the request + */ + async getCallingSettingsRequestOpts(requestParameters: GetCallingSettingsRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getCallingSettings().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/callingSettings`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Return WhatsApp Calling API settings for this channel (beta). Proxies upstream `GET /calling/settings`. **Prerequisites** - Number must be eligible for Meta Calling (Cloud API; not COEX) - Trial / `subscriptionBlocked` channels receive **403** plain text - You need your own WebRTC or SIP stack; 1msg is a **signaling proxy** only and does **not** store call history or recordings See the **Calling** tag overview for inbound/outbound flows and webhooks. + * Get calling settings + */ + async getCallingSettingsRaw(requestParameters: GetCallingSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getCallingSettingsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CallingSettingsFromJSON(jsonValue)); + } + + /** + * Return WhatsApp Calling API settings for this channel (beta). Proxies upstream `GET /calling/settings`. **Prerequisites** - Number must be eligible for Meta Calling (Cloud API; not COEX) - Trial / `subscriptionBlocked` channels receive **403** plain text - You need your own WebRTC or SIP stack; 1msg is a **signaling proxy** only and does **not** store call history or recordings See the **Calling** tag overview for inbound/outbound flows and webhooks. + * Get calling settings + */ + async getCallingSettings(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getCallingSettingsRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for initiateCall without sending the request + */ + async initiateCallRequestOpts(requestParameters: InitiateCallOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling initiateCall().' + ); + } + + if (requestParameters['initiateCallRequest'] == null) { + throw new runtime.RequiredError( + 'initiateCallRequest', + 'Required parameter "initiateCallRequest" was null or undefined when calling initiateCall().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/initiateCall`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: InitiateCallRequestToJSON(requestParameters['initiateCallRequest']), + }; + } + + /** + * Perform a WhatsApp Calling action (beta). Proxies upstream `POST /calling/calls`. Despite the historical path name `/initiateCall`, this endpoint handles **all** call actions: | action | Use | Required | |--------|-----|----------| | `connect` | Outbound business → user | `to` + `session` (`sdp_type: offer`) | | `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + `session` (`sdp_type: answer`) | | `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) | | `reject` | Decline inbound | `call_id` | | `terminate` | Hang up | `call_id` | **SDP / media (critical)** - `accept` / `pre_accept` require a **WebRTC-generated SDP answer**. - Do **not** send Meta\'s offer SDP back as the answer. - Postman (or curl) alone **cannot** establish real media — you need a WebRTC or SIP stack. 1msg only proxies signaling. Answer within ~**30–60 seconds** of an inbound `connect` webhook or Meta terminates as unanswered. Common Meta errors include Calling not enabled (`138000`), no permission (`138006`), SDP validation failures. **Outbound** requires a prior Call Permission Request (CPR) acceptance. See the **Calling** tag overview for the full outbound flow and CPR limits. Trial / `subscriptionBlocked` → **403** plain text. Upstream failures often return HTTP 200 with `{ \"response\": { \"error\": \"\" } }`. `Invalid API KEY` is used only when Meta/360 body actually indicates an invalid API key or token. Other upstream 401s (notably call permission `138006`) pass through `error.message` / `error_user_msg`. + * Call action (connect / pre_accept / accept / reject / terminate) + */ + async initiateCallRaw(requestParameters: InitiateCallOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.initiateCallRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => InitiateCallResponseFromJSON(jsonValue)); + } + + /** + * Perform a WhatsApp Calling action (beta). Proxies upstream `POST /calling/calls`. Despite the historical path name `/initiateCall`, this endpoint handles **all** call actions: | action | Use | Required | |--------|-----|----------| | `connect` | Outbound business → user | `to` + `session` (`sdp_type: offer`) | | `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + `session` (`sdp_type: answer`) | | `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) | | `reject` | Decline inbound | `call_id` | | `terminate` | Hang up | `call_id` | **SDP / media (critical)** - `accept` / `pre_accept` require a **WebRTC-generated SDP answer**. - Do **not** send Meta\'s offer SDP back as the answer. - Postman (or curl) alone **cannot** establish real media — you need a WebRTC or SIP stack. 1msg only proxies signaling. Answer within ~**30–60 seconds** of an inbound `connect` webhook or Meta terminates as unanswered. Common Meta errors include Calling not enabled (`138000`), no permission (`138006`), SDP validation failures. **Outbound** requires a prior Call Permission Request (CPR) acceptance. See the **Calling** tag overview for the full outbound flow and CPR limits. Trial / `subscriptionBlocked` → **403** plain text. Upstream failures often return HTTP 200 with `{ \"response\": { \"error\": \"\" } }`. `Invalid API KEY` is used only when Meta/360 body actually indicates an invalid API key or token. Other upstream 401s (notably call permission `138006`) pass through `error.message` / `error_user_msg`. + * Call action (connect / pre_accept / accept / reject / terminate) + */ + async initiateCall(token: string, initiateCallRequest: InitiateCallRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.initiateCallRaw({ token: token, initiateCallRequest: initiateCallRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for updateCallingSettings without sending the request + */ + async updateCallingSettingsRequestOpts(requestParameters: UpdateCallingSettingsRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling updateCallingSettings().' + ); + } + + if (requestParameters['callingSettings'] == null) { + throw new runtime.RequiredError( + 'callingSettings', + 'Required parameter "callingSettings" was null or undefined when calling updateCallingSettings().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/callingSettings`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CallingSettingsToJSON(requestParameters['callingSettings']), + }; + } + + /** + * Enable, disable, or update WhatsApp Calling settings (beta). Proxies upstream `POST /calling/settings`. Body is forwarded as-is (1msg does not validate fields). **Common fields under `calling`** - `status` (`ENABLED` | `DISABLED`) — required to turn calling on/off - `call_icon_visibility` (`DEFAULT` | `DISABLE_ALL`) — optional - `callback_permission_status` (`ENABLED` | `DISABLED`) — optional; when enabled, inbound user calls grant callback permission - `call_hours` — optional hours / timezone object - `sip` — optional SIP trunk; when SIP is ENABLED, Graph call actions and calling webhooks are not used - `srtp_key_exchange_protocol` (`DTLS` | `SDES`) — SDES only with SIP - `video.status` — optional Meta may accept only one feature group per request — prefer focused updates (e.g. enable status first, then SIP). Trial / `subscriptionBlocked` → **403** plain text. + * Update calling settings + */ + async updateCallingSettingsRaw(requestParameters: UpdateCallingSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.updateCallingSettingsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UpdateCallingSettings200ResponseFromJSON(jsonValue)); + } + + /** + * Enable, disable, or update WhatsApp Calling settings (beta). Proxies upstream `POST /calling/settings`. Body is forwarded as-is (1msg does not validate fields). **Common fields under `calling`** - `status` (`ENABLED` | `DISABLED`) — required to turn calling on/off - `call_icon_visibility` (`DEFAULT` | `DISABLE_ALL`) — optional - `callback_permission_status` (`ENABLED` | `DISABLED`) — optional; when enabled, inbound user calls grant callback permission - `call_hours` — optional hours / timezone object - `sip` — optional SIP trunk; when SIP is ENABLED, Graph call actions and calling webhooks are not used - `srtp_key_exchange_protocol` (`DTLS` | `SDES`) — SDES only with SIP - `video.status` — optional Meta may accept only one feature group per request — prefer focused updates (e.g. enable status first, then SIP). Trial / `subscriptionBlocked` → **403** plain text. + * Update calling settings + */ + async updateCallingSettings(token: string, callingSettings: CallingSettings, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.updateCallingSettingsRaw({ token: token, callingSettings: callingSettings }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/CatalogApi.ts b/quality/mcp/vendor/apis/CatalogApi.ts new file mode 100644 index 0000000..efdd81c --- /dev/null +++ b/quality/mcp/vendor/apis/CatalogApi.ts @@ -0,0 +1,228 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type CreateCommerce200Response, + CreateCommerce200ResponseFromJSON, + CreateCommerce200ResponseToJSON, +} from '../models/CreateCommerce200Response'; +import { + type CreateCommerceRequest, + CreateCommerceRequestFromJSON, + CreateCommerceRequestToJSON, +} from '../models/CreateCommerceRequest'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type GetCommerce200ResponseInner, + GetCommerce200ResponseInnerFromJSON, + GetCommerce200ResponseInnerToJSON, +} from '../models/GetCommerce200ResponseInner'; + +export interface CreateCommerceOperationRequest { + token: string; + createCommerceRequest: CreateCommerceRequest; +} + +export interface GetCommerceRequest { + token: string; +} + +/** + * CatalogApi - interface + * + * @export + * @interface CatalogApiInterface + */ +export interface CatalogApiInterface { + /** + * Creates request options for createCommerce without sending the request + * @param {string} token JWT token or API key for authorization + * @param {CreateCommerceRequest} createCommerceRequest + * @throws {RequiredError} + * @memberof CatalogApiInterface + */ + createCommerceRequestOpts(requestParameters: CreateCommerceOperationRequest): Promise; + + /** + * Update catalog/cart commerce settings via the `params` object. - `params.is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `params.is_cart_enabled` — enable cart (`true`) or disable it (`false`). Blocked when the channel subscription limit is exceeded. Requires a commerce-capable channel (Cloud Functions `/commerceWAV2`). + * @summary Set Commerce Settings + * @param {string} token JWT token or API key for authorization + * @param {CreateCommerceRequest} createCommerceRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CatalogApiInterface + */ + createCommerceRaw(requestParameters: CreateCommerceOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Update catalog/cart commerce settings via the `params` object. - `params.is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `params.is_cart_enabled` — enable cart (`true`) or disable it (`false`). Blocked when the channel subscription limit is exceeded. Requires a commerce-capable channel (Cloud Functions `/commerceWAV2`). + * Set Commerce Settings + */ + createCommerce(token: string, createCommerceRequest: CreateCommerceRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for getCommerce without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof CatalogApiInterface + */ + getCommerceRequestOpts(requestParameters: GetCommerceRequest): Promise; + + /** + * Returns catalog/cart commerce settings for the channel. Fails with a provider error when the WABA has no catalog linked — that is an API/account limitation, not an MCP schema bug. - `is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `is_cart_enabled` — enable cart (`true`) or disable it (`false`). + * @summary Get Commerce Settings + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CatalogApiInterface + */ + getCommerceRaw(requestParameters: GetCommerceRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; + + /** + * Returns catalog/cart commerce settings for the channel. Fails with a provider error when the WABA has no catalog linked — that is an API/account limitation, not an MCP schema bug. - `is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `is_cart_enabled` — enable cart (`true`) or disable it (`false`). + * Get Commerce Settings + */ + getCommerce(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + +} + +/** + * + */ +export class CatalogApi extends runtime.BaseAPI implements CatalogApiInterface { + + /** + * Creates request options for createCommerce without sending the request + */ + async createCommerceRequestOpts(requestParameters: CreateCommerceOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createCommerce().' + ); + } + + if (requestParameters['createCommerceRequest'] == null) { + throw new runtime.RequiredError( + 'createCommerceRequest', + 'Required parameter "createCommerceRequest" was null or undefined when calling createCommerce().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/commerce`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CreateCommerceRequestToJSON(requestParameters['createCommerceRequest']), + }; + } + + /** + * Update catalog/cart commerce settings via the `params` object. - `params.is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `params.is_cart_enabled` — enable cart (`true`) or disable it (`false`). Blocked when the channel subscription limit is exceeded. Requires a commerce-capable channel (Cloud Functions `/commerceWAV2`). + * Set Commerce Settings + */ + async createCommerceRaw(requestParameters: CreateCommerceOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createCommerceRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateCommerce200ResponseFromJSON(jsonValue)); + } + + /** + * Update catalog/cart commerce settings via the `params` object. - `params.is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `params.is_cart_enabled` — enable cart (`true`) or disable it (`false`). Blocked when the channel subscription limit is exceeded. Requires a commerce-capable channel (Cloud Functions `/commerceWAV2`). + * Set Commerce Settings + */ + async createCommerce(token: string, createCommerceRequest: CreateCommerceRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createCommerceRaw({ token: token, createCommerceRequest: createCommerceRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getCommerce without sending the request + */ + async getCommerceRequestOpts(requestParameters: GetCommerceRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getCommerce().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/commerce`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Returns catalog/cart commerce settings for the channel. Fails with a provider error when the WABA has no catalog linked — that is an API/account limitation, not an MCP schema bug. - `is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `is_cart_enabled` — enable cart (`true`) or disable it (`false`). + * Get Commerce Settings + */ + async getCommerceRaw(requestParameters: GetCommerceRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const requestOptions = await this.getCommerceRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(GetCommerce200ResponseInnerFromJSON)); + } + + /** + * Returns catalog/cart commerce settings for the channel. Fails with a provider error when the WABA has no catalog linked — that is an API/account limitation, not an MCP schema bug. - `is_catalog_visible` — show catalog storefront icon (`true`) or hide it (`false`). - `is_cart_enabled` — enable cart (`true`) or disable it (`false`). + * Get Commerce Settings + */ + async getCommerce(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.getCommerceRaw({ token: token }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/ChannelApi.ts b/quality/mcp/vendor/apis/ChannelApi.ts new file mode 100644 index 0000000..07471d2 --- /dev/null +++ b/quality/mcp/vendor/apis/ChannelApi.ts @@ -0,0 +1,492 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type ConversationalAutomation, + ConversationalAutomationFromJSON, + ConversationalAutomationToJSON, +} from '../models/ConversationalAutomation'; +import { + type CreateSettingsRequest, + CreateSettingsRequestFromJSON, + CreateSettingsRequestToJSON, +} from '../models/CreateSettingsRequest'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type GetStatus200Response, + GetStatus200ResponseFromJSON, + GetStatus200ResponseToJSON, +} from '../models/GetStatus200Response'; +import { + type ListSettings200Response, + ListSettings200ResponseFromJSON, + ListSettings200ResponseToJSON, +} from '../models/ListSettings200Response'; + +export interface CreateSettingsOperationRequest { + token: string; + createSettingsRequest: CreateSettingsRequest; +} + +export interface GetConversationalAutomationRequest { + token: string; +} + +export interface GetStatusRequest { + token: string; +} + +export interface ListSettingsRequest { + token: string; +} + +export interface SetConversationalAutomationRequest { + token: string; + conversationalAutomation: ConversationalAutomation; +} + +/** + * ChannelApi - interface + * + * @export + * @interface ChannelApiInterface + */ +export interface ChannelApiInterface { + /** + * Creates request options for createSettings without sending the request + * @param {string} token JWT token or API key for authorization + * @param {CreateSettingsRequest} createSettingsRequest + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + createSettingsRequestOpts(requestParameters: CreateSettingsOperationRequest): Promise; + + /** + * Update channel settings. Only send fields you want to change. `webhookUrl` **replaces** the stored list (one URL string, or up to 5 URLs as an array). Passing an empty value clears webhooks. Repeating the same URL(s) also clears a sticky auto-disable. With several URLs, send the complete array to re-enable one of them — omitting a URL removes it from the channel. + * @summary Update channel settings + * @param {string} token JWT token or API key for authorization + * @param {CreateSettingsRequest} createSettingsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + createSettingsRaw(requestParameters: CreateSettingsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Update channel settings. Only send fields you want to change. `webhookUrl` **replaces** the stored list (one URL string, or up to 5 URLs as an array). Passing an empty value clears webhooks. Repeating the same URL(s) also clears a sticky auto-disable. With several URLs, send the complete array to re-enable one of them — omitting a URL removes it from the channel. + * Update channel settings + */ + createSettings(token: string, createSettingsRequest: CreateSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for getConversationalAutomation without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + getConversationalAutomationRequestOpts(requestParameters: GetConversationalAutomationRequest): Promise; + + /** + * Get WhatsApp conversational components for the channel (welcome message, ice-breaker prompts, and slash commands). Proxies Meta/360dialog `GET /conversational_automation`. When `enable_welcome_message` is true and a user opens chat for the first time, Meta delivers a webhook message with `type: request_welcome`. The inbound formatter exposes that as `type: \"request_welcome\"` and `meta.request_welcome: true` so your webhook can send a custom welcome reply. + * @summary Get conversational automation settings + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + getConversationalAutomationRaw(requestParameters: GetConversationalAutomationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Get WhatsApp conversational components for the channel (welcome message, ice-breaker prompts, and slash commands). Proxies Meta/360dialog `GET /conversational_automation`. When `enable_welcome_message` is true and a user opens chat for the first time, Meta delivers a webhook message with `type: request_welcome`. The inbound formatter exposes that as `type: \"request_welcome\"` and `meta.request_welcome: true` so your webhook can send a custom welcome reply. + * Get conversational automation settings + */ + getConversationalAutomation(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for getStatus without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + getStatusRequestOpts(requestParameters: GetStatusRequest): Promise; + + /** + * Return WhatsApp Cloud API connection status for this channel (`connected` when the number can send). May include `accountStatus`, messaging limit, and MM Lite availability. Optional `health_status_warning` appears only when Meta Graph health could not be loaded and a soft `connected` response is returned. Omitted on the normal path and when health details are not applicable for the channel — never an internal provider/transport name. + * @summary Get channel status + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + getStatusRaw(requestParameters: GetStatusRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Return WhatsApp Cloud API connection status for this channel (`connected` when the number can send). May include `accountStatus`, messaging limit, and MM Lite availability. Optional `health_status_warning` appears only when Meta Graph health could not be loaded and a soft `connected` response is returned. Omitted on the normal path and when health details are not applicable for the channel — never an internal provider/transport name. + * Get channel status + */ + getStatus(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for listSettings without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + listSettingsRequestOpts(requestParameters: ListSettingsRequest): Promise; + + /** + * Return channel settings: webhook URL(s), delivery flags, and analytics toggles. Auto-disable / circuit state matches `GET /webhook`. **Prefer `webhookStatuses`** (one row per configured URL). The breaker is per URL, not per channel. Channel-level `haveDisabledWebhooks` is `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled — not \"the channel is dead\". Live URLs still receive events. Top-level `circuitState` / `reason` / `fails` / `trips` are a compatibility summary of one URL (first sticky-disabled, else first configured). This is the same `GET /settings` endpoint as the REST API (previously missing from MCP). + * @summary Get channel settings + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + listSettingsRaw(requestParameters: ListSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Return channel settings: webhook URL(s), delivery flags, and analytics toggles. Auto-disable / circuit state matches `GET /webhook`. **Prefer `webhookStatuses`** (one row per configured URL). The breaker is per URL, not per channel. Channel-level `haveDisabledWebhooks` is `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled — not \"the channel is dead\". Live URLs still receive events. Top-level `circuitState` / `reason` / `fails` / `trips` are a compatibility summary of one URL (first sticky-disabled, else first configured). This is the same `GET /settings` endpoint as the REST API (previously missing from MCP). + * Get channel settings + */ + listSettings(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for setConversationalAutomation without sending the request + * @param {string} token JWT token or API key for authorization + * @param {ConversationalAutomation} conversationalAutomation + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + setConversationalAutomationRequestOpts(requestParameters: SetConversationalAutomationRequest): Promise; + + /** + * Update WhatsApp conversational components. Allowed body fields (others are ignored): - `enable_welcome_message` (boolean) - `prompts` (string[], max 4, each ≤ 80 chars) - `commands` (`{ command_name, command_description }[]`) Proxies Meta/360dialog `POST /conversational_automation`. + * @summary Set conversational automation settings + * @param {string} token JWT token or API key for authorization + * @param {ConversationalAutomation} conversationalAutomation + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ChannelApiInterface + */ + setConversationalAutomationRaw(requestParameters: SetConversationalAutomationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Update WhatsApp conversational components. Allowed body fields (others are ignored): - `enable_welcome_message` (boolean) - `prompts` (string[], max 4, each ≤ 80 chars) - `commands` (`{ command_name, command_description }[]`) Proxies Meta/360dialog `POST /conversational_automation`. + * Set conversational automation settings + */ + setConversationalAutomation(token: string, conversationalAutomation: ConversationalAutomation, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + +} + +/** + * + */ +export class ChannelApi extends runtime.BaseAPI implements ChannelApiInterface { + + /** + * Creates request options for createSettings without sending the request + */ + async createSettingsRequestOpts(requestParameters: CreateSettingsOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createSettings().' + ); + } + + if (requestParameters['createSettingsRequest'] == null) { + throw new runtime.RequiredError( + 'createSettingsRequest', + 'Required parameter "createSettingsRequest" was null or undefined when calling createSettings().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/settings`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CreateSettingsRequestToJSON(requestParameters['createSettingsRequest']), + }; + } + + /** + * Update channel settings. Only send fields you want to change. `webhookUrl` **replaces** the stored list (one URL string, or up to 5 URLs as an array). Passing an empty value clears webhooks. Repeating the same URL(s) also clears a sticky auto-disable. With several URLs, send the complete array to re-enable one of them — omitting a URL removes it from the channel. + * Update channel settings + */ + async createSettingsRaw(requestParameters: CreateSettingsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createSettingsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Update channel settings. Only send fields you want to change. `webhookUrl` **replaces** the stored list (one URL string, or up to 5 URLs as an array). Passing an empty value clears webhooks. Repeating the same URL(s) also clears a sticky auto-disable. With several URLs, send the complete array to re-enable one of them — omitting a URL removes it from the channel. + * Update channel settings + */ + async createSettings(token: string, createSettingsRequest: CreateSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.createSettingsRaw({ token: token, createSettingsRequest: createSettingsRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getConversationalAutomation without sending the request + */ + async getConversationalAutomationRequestOpts(requestParameters: GetConversationalAutomationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getConversationalAutomation().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/conversationalAutomation`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get WhatsApp conversational components for the channel (welcome message, ice-breaker prompts, and slash commands). Proxies Meta/360dialog `GET /conversational_automation`. When `enable_welcome_message` is true and a user opens chat for the first time, Meta delivers a webhook message with `type: request_welcome`. The inbound formatter exposes that as `type: \"request_welcome\"` and `meta.request_welcome: true` so your webhook can send a custom welcome reply. + * Get conversational automation settings + */ + async getConversationalAutomationRaw(requestParameters: GetConversationalAutomationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getConversationalAutomationRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ConversationalAutomationFromJSON(jsonValue)); + } + + /** + * Get WhatsApp conversational components for the channel (welcome message, ice-breaker prompts, and slash commands). Proxies Meta/360dialog `GET /conversational_automation`. When `enable_welcome_message` is true and a user opens chat for the first time, Meta delivers a webhook message with `type: request_welcome`. The inbound formatter exposes that as `type: \"request_welcome\"` and `meta.request_welcome: true` so your webhook can send a custom welcome reply. + * Get conversational automation settings + */ + async getConversationalAutomation(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getConversationalAutomationRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getStatus without sending the request + */ + async getStatusRequestOpts(requestParameters: GetStatusRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getStatus().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/status`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Return WhatsApp Cloud API connection status for this channel (`connected` when the number can send). May include `accountStatus`, messaging limit, and MM Lite availability. Optional `health_status_warning` appears only when Meta Graph health could not be loaded and a soft `connected` response is returned. Omitted on the normal path and when health details are not applicable for the channel — never an internal provider/transport name. + * Get channel status + */ + async getStatusRaw(requestParameters: GetStatusRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getStatusRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => GetStatus200ResponseFromJSON(jsonValue)); + } + + /** + * Return WhatsApp Cloud API connection status for this channel (`connected` when the number can send). May include `accountStatus`, messaging limit, and MM Lite availability. Optional `health_status_warning` appears only when Meta Graph health could not be loaded and a soft `connected` response is returned. Omitted on the normal path and when health details are not applicable for the channel — never an internal provider/transport name. + * Get channel status + */ + async getStatus(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getStatusRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listSettings without sending the request + */ + async listSettingsRequestOpts(requestParameters: ListSettingsRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling listSettings().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/settings`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Return channel settings: webhook URL(s), delivery flags, and analytics toggles. Auto-disable / circuit state matches `GET /webhook`. **Prefer `webhookStatuses`** (one row per configured URL). The breaker is per URL, not per channel. Channel-level `haveDisabledWebhooks` is `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled — not \"the channel is dead\". Live URLs still receive events. Top-level `circuitState` / `reason` / `fails` / `trips` are a compatibility summary of one URL (first sticky-disabled, else first configured). This is the same `GET /settings` endpoint as the REST API (previously missing from MCP). + * Get channel settings + */ + async listSettingsRaw(requestParameters: ListSettingsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.listSettingsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ListSettings200ResponseFromJSON(jsonValue)); + } + + /** + * Return channel settings: webhook URL(s), delivery flags, and analytics toggles. Auto-disable / circuit state matches `GET /webhook`. **Prefer `webhookStatuses`** (one row per configured URL). The breaker is per URL, not per channel. Channel-level `haveDisabledWebhooks` is `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled — not \"the channel is dead\". Live URLs still receive events. Top-level `circuitState` / `reason` / `fails` / `trips` are a compatibility summary of one URL (first sticky-disabled, else first configured). This is the same `GET /settings` endpoint as the REST API (previously missing from MCP). + * Get channel settings + */ + async listSettings(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listSettingsRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for setConversationalAutomation without sending the request + */ + async setConversationalAutomationRequestOpts(requestParameters: SetConversationalAutomationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling setConversationalAutomation().' + ); + } + + if (requestParameters['conversationalAutomation'] == null) { + throw new runtime.RequiredError( + 'conversationalAutomation', + 'Required parameter "conversationalAutomation" was null or undefined when calling setConversationalAutomation().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/conversationalAutomation`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: ConversationalAutomationToJSON(requestParameters['conversationalAutomation']), + }; + } + + /** + * Update WhatsApp conversational components. Allowed body fields (others are ignored): - `enable_welcome_message` (boolean) - `prompts` (string[], max 4, each ≤ 80 chars) - `commands` (`{ command_name, command_description }[]`) Proxies Meta/360dialog `POST /conversational_automation`. + * Set conversational automation settings + */ + async setConversationalAutomationRaw(requestParameters: SetConversationalAutomationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.setConversationalAutomationRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Update WhatsApp conversational components. Allowed body fields (others are ignored): - `enable_welcome_message` (boolean) - `prompts` (string[], max 4, each ≤ 80 chars) - `commands` (`{ command_name, command_description }[]`) Proxies Meta/360dialog `POST /conversational_automation`. + * Set conversational automation settings + */ + async setConversationalAutomation(token: string, conversationalAutomation: ConversationalAutomation, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.setConversationalAutomationRaw({ token: token, conversationalAutomation: conversationalAutomation }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/FlowsApi.ts b/quality/mcp/vendor/apis/FlowsApi.ts new file mode 100644 index 0000000..3348935 --- /dev/null +++ b/quality/mcp/vendor/apis/FlowsApi.ts @@ -0,0 +1,1177 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type CreateCommerce200Response, + CreateCommerce200ResponseFromJSON, + CreateCommerce200ResponseToJSON, +} from '../models/CreateCommerce200Response'; +import { + type CreateFlowsRequest, + CreateFlowsRequestFromJSON, + CreateFlowsRequestToJSON, +} from '../models/CreateFlowsRequest'; +import { + type CreateGroups200Response, + CreateGroups200ResponseFromJSON, + CreateGroups200ResponseToJSON, +} from '../models/CreateGroups200Response'; +import { + type CreateReadMessage401Response, + CreateReadMessage401ResponseFromJSON, + CreateReadMessage401ResponseToJSON, +} from '../models/CreateReadMessage401Response'; +import { + type DeleteFlowsFlowId200Response, + DeleteFlowsFlowId200ResponseFromJSON, + DeleteFlowsFlowId200ResponseToJSON, +} from '../models/DeleteFlowsFlowId200Response'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type GetWhatsappBusinessEncryption200Response, + GetWhatsappBusinessEncryption200ResponseFromJSON, + GetWhatsappBusinessEncryption200ResponseToJSON, +} from '../models/GetWhatsappBusinessEncryption200Response'; +import { + type ListFlows200Response, + ListFlows200ResponseFromJSON, + ListFlows200ResponseToJSON, +} from '../models/ListFlows200Response'; +import { + type PatchFlowsFlowIdAssets200Response, + PatchFlowsFlowIdAssets200ResponseFromJSON, + PatchFlowsFlowIdAssets200ResponseToJSON, +} from '../models/PatchFlowsFlowIdAssets200Response'; +import { + type PatchFlowsFlowIdAssetsRequest, + PatchFlowsFlowIdAssetsRequestFromJSON, + PatchFlowsFlowIdAssetsRequestToJSON, +} from '../models/PatchFlowsFlowIdAssetsRequest'; +import { + type PatchFlowsFlowIdMetadataRequest, + PatchFlowsFlowIdMetadataRequestFromJSON, + PatchFlowsFlowIdMetadataRequestToJSON, +} from '../models/PatchFlowsFlowIdMetadataRequest'; +import { + type SetWhatsappBusinessEncryptionRequest, + SetWhatsappBusinessEncryptionRequestFromJSON, + SetWhatsappBusinessEncryptionRequestToJSON, +} from '../models/SetWhatsappBusinessEncryptionRequest'; + +export interface CreateFlowsOperationRequest { + token: string; + createFlowsRequest: CreateFlowsRequest; + wabaAccountId?: string; +} + +export interface CreateFlowsFlowIdDeprecateRequest { + flowId: string; + token: string; + wabaAccountId?: string; +} + +export interface CreateFlowsFlowIdPublishRequest { + flowId: string; + token: string; + wabaAccountId?: string; +} + +export interface DeleteFlowsFlowIdRequest { + flowId: string; + token: string; + wabaAccountId?: string; +} + +export interface GetFlowsFlowIdRequest { + flowId: string; + token: string; + wabaAccountId?: string; +} + +export interface GetFlowsFlowIdPreviewRequest { + flowId: string; + token: string; + wabaAccountId?: string; +} + +export interface GetWhatsappBusinessEncryptionRequest { + token: string; +} + +export interface ListFlowsRequest { + token: string; + wabaAccountId?: string; + fields?: string; +} + +export interface PatchFlowsFlowIdAssetsOperationRequest { + flowId: string; + token: string; + patchFlowsFlowIdAssetsRequest: PatchFlowsFlowIdAssetsRequest; + wabaAccountId?: string; +} + +export interface PatchFlowsFlowIdMetadataOperationRequest { + flowId: string; + token: string; + patchFlowsFlowIdMetadataRequest: PatchFlowsFlowIdMetadataRequest; + wabaAccountId?: string; +} + +export interface SetWhatsappBusinessEncryptionOperationRequest { + token: string; + setWhatsappBusinessEncryptionRequest: SetWhatsappBusinessEncryptionRequest; +} + +/** + * FlowsApi - interface + * + * @export + * @interface FlowsApiInterface + */ +export interface FlowsApiInterface { + /** + * Creates request options for createFlows without sending the request + * @param {string} token JWT token or API key for authorization + * @param {CreateFlowsRequest} createFlowsRequest + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + createFlowsRequestOpts(requestParameters: CreateFlowsOperationRequest): Promise; + + /** + * Create a WhatsApp Flow (`name` + `categories` required). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * @summary Create Flow + * @param {string} token JWT token or API key for authorization + * @param {CreateFlowsRequest} createFlowsRequest + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + createFlowsRaw(requestParameters: CreateFlowsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Create a WhatsApp Flow (`name` + `categories` required). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Create Flow + */ + createFlows(token: string, createFlowsRequest: CreateFlowsRequest, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for createFlowsFlowIdDeprecate without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + createFlowsFlowIdDeprecateRequestOpts(requestParameters: CreateFlowsFlowIdDeprecateRequest): Promise; + + /** + * + * @summary Deprecate Flow + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + createFlowsFlowIdDeprecateRaw(requestParameters: CreateFlowsFlowIdDeprecateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Deprecate Flow + */ + createFlowsFlowIdDeprecate(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for createFlowsFlowIdPublish without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + createFlowsFlowIdPublishRequestOpts(requestParameters: CreateFlowsFlowIdPublishRequest): Promise; + + /** + * + * @summary Publish Flow + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + createFlowsFlowIdPublishRaw(requestParameters: CreateFlowsFlowIdPublishRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Publish Flow + */ + createFlowsFlowIdPublish(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for deleteFlowsFlowId without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + deleteFlowsFlowIdRequestOpts(requestParameters: DeleteFlowsFlowIdRequest): Promise; + + /** + * + * @summary Delete Flow + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + deleteFlowsFlowIdRaw(requestParameters: DeleteFlowsFlowIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Delete Flow + */ + deleteFlowsFlowId(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for getFlowsFlowId without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + getFlowsFlowIdRequestOpts(requestParameters: GetFlowsFlowIdRequest): Promise; + + /** + * + * @summary Get Flow Details + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + getFlowsFlowIdRaw(requestParameters: GetFlowsFlowIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Get Flow Details + */ + getFlowsFlowId(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for getFlowsFlowIdPreview without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + getFlowsFlowIdPreviewRequestOpts(requestParameters: GetFlowsFlowIdPreviewRequest): Promise; + + /** + * + * @summary Preview Flow + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + getFlowsFlowIdPreviewRaw(requestParameters: GetFlowsFlowIdPreviewRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Preview Flow + */ + getFlowsFlowIdPreview(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for getWhatsappBusinessEncryption without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + getWhatsappBusinessEncryptionRequestOpts(requestParameters: GetWhatsappBusinessEncryptionRequest): Promise; + + /** + * Retrieve the WhatsApp business public key and signature status for this channel\'s phone number. Required before publishing or sending Flows that use data encryption. + * @summary Get business encryption public key + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + getWhatsappBusinessEncryptionRaw(requestParameters: GetWhatsappBusinessEncryptionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Retrieve the WhatsApp business public key and signature status for this channel\'s phone number. Required before publishing or sending Flows that use data encryption. + * Get business encryption public key + */ + getWhatsappBusinessEncryption(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for listFlows without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {string} [fields] Comma-separated Flow fields to return + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + listFlowsRequestOpts(requestParameters: ListFlowsRequest): Promise; + + /** + * List WhatsApp Flows for the WABA. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * @summary List Flows + * @param {string} token JWT token or API key for authorization + * @param {string} [wabaAccountId] + * @param {string} [fields] Comma-separated Flow fields to return + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + listFlowsRaw(requestParameters: ListFlowsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * List WhatsApp Flows for the WABA. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * List Flows + */ + listFlows(token: string, wabaAccountId?: string, fields?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for patchFlowsFlowIdAssets without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {PatchFlowsFlowIdAssetsRequest} patchFlowsFlowIdAssetsRequest + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + patchFlowsFlowIdAssetsRequestOpts(requestParameters: PatchFlowsFlowIdAssetsOperationRequest): Promise; + + /** + * Replace the Flow JSON document (`flowJson`). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * @summary Update Flow Structure + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {PatchFlowsFlowIdAssetsRequest} patchFlowsFlowIdAssetsRequest + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + patchFlowsFlowIdAssetsRaw(requestParameters: PatchFlowsFlowIdAssetsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Replace the Flow JSON document (`flowJson`). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Update Flow Structure + */ + patchFlowsFlowIdAssets(flowId: string, token: string, patchFlowsFlowIdAssetsRequest: PatchFlowsFlowIdAssetsRequest, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for patchFlowsFlowIdMetadata without sending the request + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {PatchFlowsFlowIdMetadataRequest} patchFlowsFlowIdMetadataRequest + * @param {string} [wabaAccountId] + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + patchFlowsFlowIdMetadataRequestOpts(requestParameters: PatchFlowsFlowIdMetadataOperationRequest): Promise; + + /** + * Update Flow `name`, `categories`, and/or `endpointUri`. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * @summary Update Flow Metadata + * @param {string} flowId Flow ID + * @param {string} token JWT token or API key for authorization + * @param {PatchFlowsFlowIdMetadataRequest} patchFlowsFlowIdMetadataRequest + * @param {string} [wabaAccountId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + patchFlowsFlowIdMetadataRaw(requestParameters: PatchFlowsFlowIdMetadataOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Update Flow `name`, `categories`, and/or `endpointUri`. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Update Flow Metadata + */ + patchFlowsFlowIdMetadata(flowId: string, token: string, patchFlowsFlowIdMetadataRequest: PatchFlowsFlowIdMetadataRequest, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for setWhatsappBusinessEncryption without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SetWhatsappBusinessEncryptionRequest} setWhatsappBusinessEncryptionRequest + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + setWhatsappBusinessEncryptionRequestOpts(requestParameters: SetWhatsappBusinessEncryptionOperationRequest): Promise; + + /** + * Upload and sign a 2048-bit RSA business public key (PEM) for this channel\'s phone number. Meta requires a signed key before Flow publish/send. Only one active key per number; a new upload replaces the previous key. + * @summary Set business encryption public key + * @param {string} token JWT token or API key for authorization + * @param {SetWhatsappBusinessEncryptionRequest} setWhatsappBusinessEncryptionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FlowsApiInterface + */ + setWhatsappBusinessEncryptionRaw(requestParameters: SetWhatsappBusinessEncryptionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Upload and sign a 2048-bit RSA business public key (PEM) for this channel\'s phone number. Meta requires a signed key before Flow publish/send. Only one active key per number; a new upload replaces the previous key. + * Set business encryption public key + */ + setWhatsappBusinessEncryption(token: string, setWhatsappBusinessEncryptionRequest: SetWhatsappBusinessEncryptionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + +} + +/** + * + */ +export class FlowsApi extends runtime.BaseAPI implements FlowsApiInterface { + + /** + * Creates request options for createFlows without sending the request + */ + async createFlowsRequestOpts(requestParameters: CreateFlowsOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createFlows().' + ); + } + + if (requestParameters['createFlowsRequest'] == null) { + throw new runtime.RequiredError( + 'createFlowsRequest', + 'Required parameter "createFlowsRequest" was null or undefined when calling createFlows().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CreateFlowsRequestToJSON(requestParameters['createFlowsRequest']), + }; + } + + /** + * Create a WhatsApp Flow (`name` + `categories` required). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Create Flow + */ + async createFlowsRaw(requestParameters: CreateFlowsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createFlowsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateGroups200ResponseFromJSON(jsonValue)); + } + + /** + * Create a WhatsApp Flow (`name` + `categories` required). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Create Flow + */ + async createFlows(token: string, createFlowsRequest: CreateFlowsRequest, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createFlowsRaw({ token: token, createFlowsRequest: createFlowsRequest, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for createFlowsFlowIdDeprecate without sending the request + */ + async createFlowsFlowIdDeprecateRequestOpts(requestParameters: CreateFlowsFlowIdDeprecateRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling createFlowsFlowIdDeprecate().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createFlowsFlowIdDeprecate().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}/deprecate`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Deprecate Flow + */ + async createFlowsFlowIdDeprecateRaw(requestParameters: CreateFlowsFlowIdDeprecateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createFlowsFlowIdDeprecateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Deprecate Flow + */ + async createFlowsFlowIdDeprecate(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.createFlowsFlowIdDeprecateRaw({ flowId: flowId, token: token, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for createFlowsFlowIdPublish without sending the request + */ + async createFlowsFlowIdPublishRequestOpts(requestParameters: CreateFlowsFlowIdPublishRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling createFlowsFlowIdPublish().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createFlowsFlowIdPublish().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}/publish`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Publish Flow + */ + async createFlowsFlowIdPublishRaw(requestParameters: CreateFlowsFlowIdPublishRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createFlowsFlowIdPublishRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Publish Flow + */ + async createFlowsFlowIdPublish(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.createFlowsFlowIdPublishRaw({ flowId: flowId, token: token, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for deleteFlowsFlowId without sending the request + */ + async deleteFlowsFlowIdRequestOpts(requestParameters: DeleteFlowsFlowIdRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling deleteFlowsFlowId().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling deleteFlowsFlowId().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Delete Flow + */ + async deleteFlowsFlowIdRaw(requestParameters: DeleteFlowsFlowIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.deleteFlowsFlowIdRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => DeleteFlowsFlowId200ResponseFromJSON(jsonValue)); + } + + /** + * Delete Flow + */ + async deleteFlowsFlowId(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.deleteFlowsFlowIdRaw({ flowId: flowId, token: token, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getFlowsFlowId without sending the request + */ + async getFlowsFlowIdRequestOpts(requestParameters: GetFlowsFlowIdRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling getFlowsFlowId().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getFlowsFlowId().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get Flow Details + */ + async getFlowsFlowIdRaw(requestParameters: GetFlowsFlowIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getFlowsFlowIdRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateGroups200ResponseFromJSON(jsonValue)); + } + + /** + * Get Flow Details + */ + async getFlowsFlowId(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getFlowsFlowIdRaw({ flowId: flowId, token: token, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getFlowsFlowIdPreview without sending the request + */ + async getFlowsFlowIdPreviewRequestOpts(requestParameters: GetFlowsFlowIdPreviewRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling getFlowsFlowIdPreview().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getFlowsFlowIdPreview().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}/preview`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Preview Flow + */ + async getFlowsFlowIdPreviewRaw(requestParameters: GetFlowsFlowIdPreviewRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getFlowsFlowIdPreviewRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Preview Flow + */ + async getFlowsFlowIdPreview(flowId: string, token: string, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.getFlowsFlowIdPreviewRaw({ flowId: flowId, token: token, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getWhatsappBusinessEncryption without sending the request + */ + async getWhatsappBusinessEncryptionRequestOpts(requestParameters: GetWhatsappBusinessEncryptionRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getWhatsappBusinessEncryption().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/whatsapp_business_encryption`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Retrieve the WhatsApp business public key and signature status for this channel\'s phone number. Required before publishing or sending Flows that use data encryption. + * Get business encryption public key + */ + async getWhatsappBusinessEncryptionRaw(requestParameters: GetWhatsappBusinessEncryptionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getWhatsappBusinessEncryptionRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => GetWhatsappBusinessEncryption200ResponseFromJSON(jsonValue)); + } + + /** + * Retrieve the WhatsApp business public key and signature status for this channel\'s phone number. Required before publishing or sending Flows that use data encryption. + * Get business encryption public key + */ + async getWhatsappBusinessEncryption(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getWhatsappBusinessEncryptionRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listFlows without sending the request + */ + async listFlowsRequestOpts(requestParameters: ListFlowsRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling listFlows().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + if (requestParameters['fields'] != null) { + queryParameters['fields'] = requestParameters['fields']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * List WhatsApp Flows for the WABA. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * List Flows + */ + async listFlowsRaw(requestParameters: ListFlowsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.listFlowsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ListFlows200ResponseFromJSON(jsonValue)); + } + + /** + * List WhatsApp Flows for the WABA. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * List Flows + */ + async listFlows(token: string, wabaAccountId?: string, fields?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listFlowsRaw({ token: token, wabaAccountId: wabaAccountId, fields: fields }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for patchFlowsFlowIdAssets without sending the request + */ + async patchFlowsFlowIdAssetsRequestOpts(requestParameters: PatchFlowsFlowIdAssetsOperationRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling patchFlowsFlowIdAssets().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling patchFlowsFlowIdAssets().' + ); + } + + if (requestParameters['patchFlowsFlowIdAssetsRequest'] == null) { + throw new runtime.RequiredError( + 'patchFlowsFlowIdAssetsRequest', + 'Required parameter "patchFlowsFlowIdAssetsRequest" was null or undefined when calling patchFlowsFlowIdAssets().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}/assets`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: PatchFlowsFlowIdAssetsRequestToJSON(requestParameters['patchFlowsFlowIdAssetsRequest']), + }; + } + + /** + * Replace the Flow JSON document (`flowJson`). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Update Flow Structure + */ + async patchFlowsFlowIdAssetsRaw(requestParameters: PatchFlowsFlowIdAssetsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.patchFlowsFlowIdAssetsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => PatchFlowsFlowIdAssets200ResponseFromJSON(jsonValue)); + } + + /** + * Replace the Flow JSON document (`flowJson`). Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Update Flow Structure + */ + async patchFlowsFlowIdAssets(flowId: string, token: string, patchFlowsFlowIdAssetsRequest: PatchFlowsFlowIdAssetsRequest, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.patchFlowsFlowIdAssetsRaw({ flowId: flowId, token: token, patchFlowsFlowIdAssetsRequest: patchFlowsFlowIdAssetsRequest, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for patchFlowsFlowIdMetadata without sending the request + */ + async patchFlowsFlowIdMetadataRequestOpts(requestParameters: PatchFlowsFlowIdMetadataOperationRequest): Promise { + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling patchFlowsFlowIdMetadata().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling patchFlowsFlowIdMetadata().' + ); + } + + if (requestParameters['patchFlowsFlowIdMetadataRequest'] == null) { + throw new runtime.RequiredError( + 'patchFlowsFlowIdMetadataRequest', + 'Required parameter "patchFlowsFlowIdMetadataRequest" was null or undefined when calling patchFlowsFlowIdMetadata().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['wabaAccountId'] != null) { + queryParameters['wabaAccountId'] = requestParameters['wabaAccountId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/flows/{flowId}/metadata`; + urlPath = urlPath.replace('{flowId}', encodeURIComponent(String(requestParameters['flowId']))); + + return { + path: urlPath, + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: PatchFlowsFlowIdMetadataRequestToJSON(requestParameters['patchFlowsFlowIdMetadataRequest']), + }; + } + + /** + * Update Flow `name`, `categories`, and/or `endpointUri`. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Update Flow Metadata + */ + async patchFlowsFlowIdMetadataRaw(requestParameters: PatchFlowsFlowIdMetadataOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.patchFlowsFlowIdMetadataRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Update Flow `name`, `categories`, and/or `endpointUri`. Currently returns HTTP 501 until the Meta Graph Flows rewrite is available. + * Update Flow Metadata + */ + async patchFlowsFlowIdMetadata(flowId: string, token: string, patchFlowsFlowIdMetadataRequest: PatchFlowsFlowIdMetadataRequest, wabaAccountId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.patchFlowsFlowIdMetadataRaw({ flowId: flowId, token: token, patchFlowsFlowIdMetadataRequest: patchFlowsFlowIdMetadataRequest, wabaAccountId: wabaAccountId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for setWhatsappBusinessEncryption without sending the request + */ + async setWhatsappBusinessEncryptionRequestOpts(requestParameters: SetWhatsappBusinessEncryptionOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling setWhatsappBusinessEncryption().' + ); + } + + if (requestParameters['setWhatsappBusinessEncryptionRequest'] == null) { + throw new runtime.RequiredError( + 'setWhatsappBusinessEncryptionRequest', + 'Required parameter "setWhatsappBusinessEncryptionRequest" was null or undefined when calling setWhatsappBusinessEncryption().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/whatsapp_business_encryption`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SetWhatsappBusinessEncryptionRequestToJSON(requestParameters['setWhatsappBusinessEncryptionRequest']), + }; + } + + /** + * Upload and sign a 2048-bit RSA business public key (PEM) for this channel\'s phone number. Meta requires a signed key before Flow publish/send. Only one active key per number; a new upload replaces the previous key. + * Set business encryption public key + */ + async setWhatsappBusinessEncryptionRaw(requestParameters: SetWhatsappBusinessEncryptionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.setWhatsappBusinessEncryptionRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateCommerce200ResponseFromJSON(jsonValue)); + } + + /** + * Upload and sign a 2048-bit RSA business public key (PEM) for this channel\'s phone number. Meta requires a signed key before Flow publish/send. Only one active key per number; a new upload replaces the previous key. + * Set business encryption public key + */ + async setWhatsappBusinessEncryption(token: string, setWhatsappBusinessEncryptionRequest: SetWhatsappBusinessEncryptionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.setWhatsappBusinessEncryptionRaw({ token: token, setWhatsappBusinessEncryptionRequest: setWhatsappBusinessEncryptionRequest }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/GroupsApi.ts b/quality/mcp/vendor/apis/GroupsApi.ts new file mode 100644 index 0000000..2b770cc --- /dev/null +++ b/quality/mcp/vendor/apis/GroupsApi.ts @@ -0,0 +1,719 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type CreateGroups200Response, + CreateGroups200ResponseFromJSON, + CreateGroups200ResponseToJSON, +} from '../models/CreateGroups200Response'; +import { + type CreateGroupsGroupIdRequest, + CreateGroupsGroupIdRequestFromJSON, + CreateGroupsGroupIdRequestToJSON, +} from '../models/CreateGroupsGroupIdRequest'; +import { + type CreateGroupsRequest, + CreateGroupsRequestFromJSON, + CreateGroupsRequestToJSON, +} from '../models/CreateGroupsRequest'; +import { + type DeleteGroupsGroupId200Response, + DeleteGroupsGroupId200ResponseFromJSON, + DeleteGroupsGroupId200ResponseToJSON, +} from '../models/DeleteGroupsGroupId200Response'; + +export interface CreateGroupsOperationRequest { + token: string; + createGroupsRequest: CreateGroupsRequest; +} + +export interface CreateGroupsGroupIdOperationRequest { + groupId: string; + token: string; + createGroupsGroupIdRequest: CreateGroupsGroupIdRequest; +} + +export interface CreateGroupsGroupIdInvitelinkRequest { + groupId: string; + token: string; +} + +export interface DeleteGroupsGroupIdRequest { + groupId: string; + token: string; +} + +export interface GetGroupsGroupIdRequest { + groupId: string; + token: string; + fields?: string; +} + +export interface GetGroupsGroupIdInvitelinkRequest { + groupId: string; + token: string; +} + +export interface ListGroupsRequest { + token: string; + limit?: number; + before?: string; + after?: string; +} + +/** + * GroupsApi - interface + * + * @export + * @interface GroupsApiInterface + */ +export interface GroupsApiInterface { + /** + * Creates request options for createGroups without sending the request + * @param {string} token JWT token or API key for authorization + * @param {CreateGroupsRequest} createGroupsRequest + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + createGroupsRequestOpts(requestParameters: CreateGroupsOperationRequest): Promise; + + /** + * + * @summary Create Group + * @param {string} token JWT token or API key for authorization + * @param {CreateGroupsRequest} createGroupsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + createGroupsRaw(requestParameters: CreateGroupsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Create Group + */ + createGroups(token: string, createGroupsRequest: CreateGroupsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for createGroupsGroupId without sending the request + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {CreateGroupsGroupIdRequest} createGroupsGroupIdRequest + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + createGroupsGroupIdRequestOpts(requestParameters: CreateGroupsGroupIdOperationRequest): Promise; + + /** + * Update group subject, description, and/or profile picture. Path `groupId` is the WABA group id (without `@g.us`). + * @summary Update Group Info + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {CreateGroupsGroupIdRequest} createGroupsGroupIdRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + createGroupsGroupIdRaw(requestParameters: CreateGroupsGroupIdOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Update group subject, description, and/or profile picture. Path `groupId` is the WABA group id (without `@g.us`). + * Update Group Info + */ + createGroupsGroupId(groupId: string, token: string, createGroupsGroupIdRequest: CreateGroupsGroupIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for createGroupsGroupIdInvitelink without sending the request + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + createGroupsGroupIdInvitelinkRequestOpts(requestParameters: CreateGroupsGroupIdInvitelinkRequest): Promise; + + /** + * + * @summary Reset Invite Link + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + createGroupsGroupIdInvitelinkRaw(requestParameters: CreateGroupsGroupIdInvitelinkRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Reset Invite Link + */ + createGroupsGroupIdInvitelink(groupId: string, token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for deleteGroupsGroupId without sending the request + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + deleteGroupsGroupIdRequestOpts(requestParameters: DeleteGroupsGroupIdRequest): Promise; + + /** + * + * @summary Delete Group + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + deleteGroupsGroupIdRaw(requestParameters: DeleteGroupsGroupIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Delete Group + */ + deleteGroupsGroupId(groupId: string, token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for getGroupsGroupId without sending the request + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {string} [fields] Comma-separated group fields to return + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + getGroupsGroupIdRequestOpts(requestParameters: GetGroupsGroupIdRequest): Promise; + + /** + * + * @summary Get Group Info + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {string} [fields] Comma-separated group fields to return + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + getGroupsGroupIdRaw(requestParameters: GetGroupsGroupIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Get Group Info + */ + getGroupsGroupId(groupId: string, token: string, fields?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for getGroupsGroupIdInvitelink without sending the request + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + getGroupsGroupIdInvitelinkRequestOpts(requestParameters: GetGroupsGroupIdInvitelinkRequest): Promise; + + /** + * + * @summary Get Invite Link + * @param {string} groupId + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + getGroupsGroupIdInvitelinkRaw(requestParameters: GetGroupsGroupIdInvitelinkRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Get Invite Link + */ + getGroupsGroupIdInvitelink(groupId: string, token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for listGroups without sending the request + * @param {string} token JWT token or API key for authorization + * @param {number} [limit] Max groups to return + * @param {string} [before] Cursor for the previous page + * @param {string} [after] Cursor for the next page + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + listGroupsRequestOpts(requestParameters: ListGroupsRequest): Promise; + + /** + * List WhatsApp groups for this channel. Paginate with `limit`, `before`, and `after`. + * @summary Get Groups List + * @param {string} token JWT token or API key for authorization + * @param {number} [limit] Max groups to return + * @param {string} [before] Cursor for the previous page + * @param {string} [after] Cursor for the next page + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GroupsApiInterface + */ + listGroupsRaw(requestParameters: ListGroupsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * List WhatsApp groups for this channel. Paginate with `limit`, `before`, and `after`. + * Get Groups List + */ + listGroups(token: string, limit?: number, before?: string, after?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + +} + +/** + * + */ +export class GroupsApi extends runtime.BaseAPI implements GroupsApiInterface { + + /** + * Creates request options for createGroups without sending the request + */ + async createGroupsRequestOpts(requestParameters: CreateGroupsOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createGroups().' + ); + } + + if (requestParameters['createGroupsRequest'] == null) { + throw new runtime.RequiredError( + 'createGroupsRequest', + 'Required parameter "createGroupsRequest" was null or undefined when calling createGroups().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CreateGroupsRequestToJSON(requestParameters['createGroupsRequest']), + }; + } + + /** + * Create Group + */ + async createGroupsRaw(requestParameters: CreateGroupsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createGroupsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateGroups200ResponseFromJSON(jsonValue)); + } + + /** + * Create Group + */ + async createGroups(token: string, createGroupsRequest: CreateGroupsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createGroupsRaw({ token: token, createGroupsRequest: createGroupsRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for createGroupsGroupId without sending the request + */ + async createGroupsGroupIdRequestOpts(requestParameters: CreateGroupsGroupIdOperationRequest): Promise { + if (requestParameters['groupId'] == null) { + throw new runtime.RequiredError( + 'groupId', + 'Required parameter "groupId" was null or undefined when calling createGroupsGroupId().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createGroupsGroupId().' + ); + } + + if (requestParameters['createGroupsGroupIdRequest'] == null) { + throw new runtime.RequiredError( + 'createGroupsGroupIdRequest', + 'Required parameter "createGroupsGroupIdRequest" was null or undefined when calling createGroupsGroupId().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups/{groupId}`; + urlPath = urlPath.replace('{groupId}', encodeURIComponent(String(requestParameters['groupId']))); + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CreateGroupsGroupIdRequestToJSON(requestParameters['createGroupsGroupIdRequest']), + }; + } + + /** + * Update group subject, description, and/or profile picture. Path `groupId` is the WABA group id (without `@g.us`). + * Update Group Info + */ + async createGroupsGroupIdRaw(requestParameters: CreateGroupsGroupIdOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createGroupsGroupIdRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Update group subject, description, and/or profile picture. Path `groupId` is the WABA group id (without `@g.us`). + * Update Group Info + */ + async createGroupsGroupId(groupId: string, token: string, createGroupsGroupIdRequest: CreateGroupsGroupIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.createGroupsGroupIdRaw({ groupId: groupId, token: token, createGroupsGroupIdRequest: createGroupsGroupIdRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for createGroupsGroupIdInvitelink without sending the request + */ + async createGroupsGroupIdInvitelinkRequestOpts(requestParameters: CreateGroupsGroupIdInvitelinkRequest): Promise { + if (requestParameters['groupId'] == null) { + throw new runtime.RequiredError( + 'groupId', + 'Required parameter "groupId" was null or undefined when calling createGroupsGroupIdInvitelink().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createGroupsGroupIdInvitelink().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups/{groupId}/inviteLink`; + urlPath = urlPath.replace('{groupId}', encodeURIComponent(String(requestParameters['groupId']))); + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Reset Invite Link + */ + async createGroupsGroupIdInvitelinkRaw(requestParameters: CreateGroupsGroupIdInvitelinkRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createGroupsGroupIdInvitelinkRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Reset Invite Link + */ + async createGroupsGroupIdInvitelink(groupId: string, token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.createGroupsGroupIdInvitelinkRaw({ groupId: groupId, token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for deleteGroupsGroupId without sending the request + */ + async deleteGroupsGroupIdRequestOpts(requestParameters: DeleteGroupsGroupIdRequest): Promise { + if (requestParameters['groupId'] == null) { + throw new runtime.RequiredError( + 'groupId', + 'Required parameter "groupId" was null or undefined when calling deleteGroupsGroupId().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling deleteGroupsGroupId().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups/{groupId}`; + urlPath = urlPath.replace('{groupId}', encodeURIComponent(String(requestParameters['groupId']))); + + return { + path: urlPath, + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Delete Group + */ + async deleteGroupsGroupIdRaw(requestParameters: DeleteGroupsGroupIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.deleteGroupsGroupIdRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => DeleteGroupsGroupId200ResponseFromJSON(jsonValue)); + } + + /** + * Delete Group + */ + async deleteGroupsGroupId(groupId: string, token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.deleteGroupsGroupIdRaw({ groupId: groupId, token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getGroupsGroupId without sending the request + */ + async getGroupsGroupIdRequestOpts(requestParameters: GetGroupsGroupIdRequest): Promise { + if (requestParameters['groupId'] == null) { + throw new runtime.RequiredError( + 'groupId', + 'Required parameter "groupId" was null or undefined when calling getGroupsGroupId().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getGroupsGroupId().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['fields'] != null) { + queryParameters['fields'] = requestParameters['fields']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups/{groupId}`; + urlPath = urlPath.replace('{groupId}', encodeURIComponent(String(requestParameters['groupId']))); + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get Group Info + */ + async getGroupsGroupIdRaw(requestParameters: GetGroupsGroupIdRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getGroupsGroupIdRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Get Group Info + */ + async getGroupsGroupId(groupId: string, token: string, fields?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.getGroupsGroupIdRaw({ groupId: groupId, token: token, fields: fields }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getGroupsGroupIdInvitelink without sending the request + */ + async getGroupsGroupIdInvitelinkRequestOpts(requestParameters: GetGroupsGroupIdInvitelinkRequest): Promise { + if (requestParameters['groupId'] == null) { + throw new runtime.RequiredError( + 'groupId', + 'Required parameter "groupId" was null or undefined when calling getGroupsGroupIdInvitelink().' + ); + } + + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getGroupsGroupIdInvitelink().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups/{groupId}/inviteLink`; + urlPath = urlPath.replace('{groupId}', encodeURIComponent(String(requestParameters['groupId']))); + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get Invite Link + */ + async getGroupsGroupIdInvitelinkRaw(requestParameters: GetGroupsGroupIdInvitelinkRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getGroupsGroupIdInvitelinkRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Get Invite Link + */ + async getGroupsGroupIdInvitelink(groupId: string, token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.getGroupsGroupIdInvitelinkRaw({ groupId: groupId, token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listGroups without sending the request + */ + async listGroupsRequestOpts(requestParameters: ListGroupsRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling listGroups().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['limit'] != null) { + queryParameters['limit'] = requestParameters['limit']; + } + + if (requestParameters['before'] != null) { + queryParameters['before'] = requestParameters['before']; + } + + if (requestParameters['after'] != null) { + queryParameters['after'] = requestParameters['after']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/groups`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * List WhatsApp groups for this channel. Paginate with `limit`, `before`, and `after`. + * Get Groups List + */ + async listGroupsRaw(requestParameters: ListGroupsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.listGroupsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * List WhatsApp groups for this channel. Paginate with `limit`, `before`, and `after`. + * Get Groups List + */ + async listGroups(token: string, limit?: number, before?: string, after?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.listGroupsRaw({ token: token, limit: limit, before: before, after: after }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/MessagingApi.ts b/quality/mcp/vendor/apis/MessagingApi.ts new file mode 100644 index 0000000..cc61135 --- /dev/null +++ b/quality/mcp/vendor/apis/MessagingApi.ts @@ -0,0 +1,2716 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { objectToJSON } from '../runtime'; +import { + type CreateReadMessage200Response, + CreateReadMessage200ResponseFromJSON, + CreateReadMessage200ResponseToJSON, +} from '../models/CreateReadMessage200Response'; +import { + type CreateReadMessage401Response, + CreateReadMessage401ResponseFromJSON, + CreateReadMessage401ResponseToJSON, +} from '../models/CreateReadMessage401Response'; +import { + type CreateUploadMedia200Response, + CreateUploadMedia200ResponseFromJSON, + CreateUploadMedia200ResponseToJSON, +} from '../models/CreateUploadMedia200Response'; +import { + type CreateUploadMediaRequest, + CreateUploadMediaRequestFromJSON, + CreateUploadMediaRequestToJSON, +} from '../models/CreateUploadMediaRequest'; +import { + type DeleteMediaLegacyRequest, + DeleteMediaLegacyRequestFromJSON, + DeleteMediaLegacyRequestToJSON, +} from '../models/DeleteMediaLegacyRequest'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type GetMmLiteStatus200Response, + GetMmLiteStatus200ResponseFromJSON, + GetMmLiteStatus200ResponseToJSON, +} from '../models/GetMmLiteStatus200Response'; +import { + type ListMessages200Response, + ListMessages200ResponseFromJSON, + ListMessages200ResponseToJSON, +} from '../models/ListMessages200Response'; +import { + type MessageSentResponse, + MessageSentResponseFromJSON, + MessageSentResponseToJSON, +} from '../models/MessageSentResponse'; +import { + type RetrieveMedia200Response, + RetrieveMedia200ResponseFromJSON, + RetrieveMedia200ResponseToJSON, +} from '../models/RetrieveMedia200Response'; +import { + type SendAddressMessageRequest, + SendAddressMessageRequestFromJSON, + SendAddressMessageRequestToJSON, +} from '../models/SendAddressMessageRequest'; +import { + type SendButtonRequest, + SendButtonRequestFromJSON, + SendButtonRequestToJSON, +} from '../models/SendButtonRequest'; +import { + type SendContactRequest, + SendContactRequestFromJSON, + SendContactRequestToJSON, +} from '../models/SendContactRequest'; +import { + type SendCtaUrlRequest, + SendCtaUrlRequestFromJSON, + SendCtaUrlRequestToJSON, +} from '../models/SendCtaUrlRequest'; +import { + type SendFlowRequestHeader, + SendFlowRequestHeaderFromJSON, + SendFlowRequestHeaderToJSON, +} from '../models/SendFlowRequestHeader'; +import { + type SendListRequest, + SendListRequestFromJSON, + SendListRequestToJSON, +} from '../models/SendListRequest'; +import { + type SendLocationRequestRequest, + SendLocationRequestRequestFromJSON, + SendLocationRequestRequestToJSON, +} from '../models/SendLocationRequestRequest'; +import { + type SendOrderDetailsRequest, + SendOrderDetailsRequestFromJSON, + SendOrderDetailsRequestToJSON, +} from '../models/SendOrderDetailsRequest'; +import { + type SendPaymentRequestRequest, + SendPaymentRequestRequestFromJSON, + SendPaymentRequestRequestToJSON, +} from '../models/SendPaymentRequestRequest'; +import { + type SendProductRequest, + SendProductRequestFromJSON, + SendProductRequestToJSON, +} from '../models/SendProductRequest'; +import { + type SendReactionRequest, + SendReactionRequestFromJSON, + SendReactionRequestToJSON, +} from '../models/SendReactionRequest'; +import { + type SendStickerRequest, + SendStickerRequestFromJSON, + SendStickerRequestToJSON, +} from '../models/SendStickerRequest'; +import { + type SuccessResponse, + SuccessResponseFromJSON, + SuccessResponseToJSON, +} from '../models/SuccessResponse'; + +export interface CreateReadMessageRequest { + token: string; + messageId?: string; + msgId?: string; + typingIndicator?: boolean; +} + +export interface CreateUploadMediaOperationRequest { + token: string; + createUploadMediaRequest: CreateUploadMediaRequest; +} + +export interface DeleteMediaRequest { + token: string; + mediaId: string; +} + +export interface DeleteMediaLegacyOperationRequest { + token: string; + deleteMediaLegacyRequest: DeleteMediaLegacyRequest; +} + +export interface GetMmLiteStatusRequest { + token: string; +} + +export interface ListMessagesRequest { + token: string; + chatId?: string; + limit?: number; + last?: boolean; + lastMessageNumber?: number; + firstMessageNumber?: number; + minTime?: number; + maxTime?: number; + msgId?: string; +} + +export interface RetrieveMediaRequest { + token: string; + mediaId: string; +} + +export interface SendAddressMessageOperationRequest { + token: string; + sendAddressMessageRequest: SendAddressMessageRequest; +} + +export interface SendButtonOperationRequest { + token: string; + sendButtonRequest?: SendButtonRequest; +} + +export interface SendCarouselRequest { + token: string; + body?: string; + cards?: Array<{ [key: string]: any; }>; + params?: Array; + quotedMsgId?: string; + chatId?: string; + phone?: number; +} + +export interface SendContactOperationRequest { + token: string; + sendContactRequest?: SendContactRequest; +} + +export interface SendCtaUrlOperationRequest { + token: string; + sendCtaUrlRequest: SendCtaUrlRequest; +} + +export interface SendFileRequest { + token: string; + body?: string; + filename?: string; + mediaId?: string; + mediaType?: SendFileMediaTypeEnum; + voice?: boolean; + caption?: string; + quotedMsgId?: string; + chatId?: string; + phone?: number; +} + +export interface SendFlowRequest { + token: string; + body: string; + flowId: string; + flowToken: string; + flowCta: string; + header?: SendFlowRequestHeader; + footer?: string; + flowAction?: SendFlowFlowActionEnum; + flowActionPayload?: object; + flowMessageVersion?: string; + mode?: SendFlowModeEnum; + flowActionData?: object; + flowActionScreen?: string; + quotedMsgId?: string; + chatId?: string; + phone?: number; +} + +export interface SendListOperationRequest { + token: string; + sendListRequest?: SendListRequest; +} + +export interface SendLocationRequest { + token: string; + lat: string; + lng: string; + address?: string; + name?: string; + quotedMsgId?: string; + chatId?: string; + phone?: number; +} + +export interface SendLocationRequestOperationRequest { + token: string; + sendLocationRequestRequest?: SendLocationRequestRequest; +} + +export interface SendMessageRequest { + token: string; + body: string; + quotedMsgId?: string; + chatId?: string; + phone?: number; +} + +export interface SendOrderDetailsOperationRequest { + token: string; + sendOrderDetailsRequest: SendOrderDetailsRequest; +} + +export interface SendPaymentRequestOperationRequest { + token: string; + sendPaymentRequestRequest: SendPaymentRequestRequest; +} + +export interface SendProductOperationRequest { + token: string; + sendProductRequest: SendProductRequest; +} + +export interface SendReactionOperationRequest { + token: string; + sendReactionRequest?: SendReactionRequest; +} + +export interface SendStickerOperationRequest { + token: string; + sendStickerRequest: SendStickerRequest; +} + +/** + * MessagingApi - interface + * + * @export + * @interface MessagingApiInterface + */ +export interface MessagingApiInterface { + /** + * Creates request options for createReadMessage without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} [messageId] + * @param {string} [msgId] + * @param {boolean} [typingIndicator] Show WhatsApp typing indicator (max 25s or until reply) + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + createReadMessageRequestOpts(requestParameters: CreateReadMessageRequest): Promise; + + /** + * + * @summary Mark message as read + * @param {string} token JWT token or API key for authorization + * @param {string} [messageId] + * @param {string} [msgId] + * @param {boolean} [typingIndicator] Show WhatsApp typing indicator (max 25s or until reply) + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + createReadMessageRaw(requestParameters: CreateReadMessageRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Mark message as read + */ + createReadMessage(token: string, messageId?: string, msgId?: string, typingIndicator?: boolean, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for createUploadMedia without sending the request + * @param {string} token JWT token or API key for authorization + * @param {CreateUploadMediaRequest} createUploadMediaRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + createUploadMediaRequestOpts(requestParameters: CreateUploadMediaOperationRequest): Promise; + + /** + * Upload a file to WhatsApp media storage and get a numeric `mediaId` for later use with `send_file` / `send_sticker` (`mediaId` field, not `body`). Pass the file as `body` **or** `url` — an HTTP(S) URL or a base64 data URI. The `mediaId` in the response is a numeric string (keep it as a string). + * @summary Upload media + * @param {string} token JWT token or API key for authorization + * @param {CreateUploadMediaRequest} createUploadMediaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + createUploadMediaRaw(requestParameters: CreateUploadMediaOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Upload a file to WhatsApp media storage and get a numeric `mediaId` for later use with `send_file` / `send_sticker` (`mediaId` field, not `body`). Pass the file as `body` **or** `url` — an HTTP(S) URL or a base64 data URI. The `mediaId` in the response is a numeric string (keep it as a string). + * Upload media + */ + createUploadMedia(token: string, createUploadMediaRequest: CreateUploadMediaRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for deleteMedia without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} mediaId Numeric WABA media id + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + deleteMediaRequestOpts(requestParameters: DeleteMediaRequest): Promise; + + /** + * Delete previously uploaded media by numeric `mediaId` (from `/uploadMedia`). This is the canonical deletion endpoint and uses the REST `DELETE` verb on the media resource path. The older `POST /deleteMedia` is a deprecated alias. + * @summary Delete media from WABA storage + * @param {string} token JWT token or API key for authorization + * @param {string} mediaId Numeric WABA media id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + deleteMediaRaw(requestParameters: DeleteMediaRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Delete previously uploaded media by numeric `mediaId` (from `/uploadMedia`). This is the canonical deletion endpoint and uses the REST `DELETE` verb on the media resource path. The older `POST /deleteMedia` is a deprecated alias. + * Delete media from WABA storage + */ + deleteMedia(token: string, mediaId: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for deleteMediaLegacy without sending the request + * @param {string} token JWT token or API key for authorization + * @param {DeleteMediaLegacyRequest} deleteMediaLegacyRequest + * @deprecated + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + deleteMediaLegacyRequestOpts(requestParameters: DeleteMediaLegacyOperationRequest): Promise; + + /** + * **Deprecated.** Use `DELETE /media/{mediaId}` instead. This POST alias is kept for backward compatibility with earlier integrations. New integrations should call `DELETE /media/{mediaId}`: 1msg follows REST conventions for resource deletion going forward (delete a resource with the `DELETE` verb on its resource path). + * @summary Delete media from WABA storage (deprecated alias) + * @param {string} token JWT token or API key for authorization + * @param {DeleteMediaLegacyRequest} deleteMediaLegacyRequest + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + deleteMediaLegacyRaw(requestParameters: DeleteMediaLegacyOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * **Deprecated.** Use `DELETE /media/{mediaId}` instead. This POST alias is kept for backward compatibility with earlier integrations. New integrations should call `DELETE /media/{mediaId}`: 1msg follows REST conventions for resource deletion going forward (delete a resource with the `DELETE` verb on its resource path). + * Delete media from WABA storage (deprecated alias) + * @deprecated + */ + deleteMediaLegacy(token: string, deleteMediaLegacyRequest: DeleteMediaLegacyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for getMmLiteStatus without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + getMmLiteStatusRequestOpts(requestParameters: GetMmLiteStatusRequest): Promise; + + /** + * + * @summary Get MM Lite availability and status + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + getMmLiteStatusRaw(requestParameters: GetMmLiteStatusRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Get MM Lite availability and status + */ + getMmLiteStatus(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for listMessages without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} [chatId] Filter messages for one chat (`phone@c.us`, group `@g.us`, or BSUID `@lid`) + * @param {number} [limit] Max rows to return (default 100) + * @param {boolean} [last] If true, return the latest messages (DESC). Default false. + * @param {number} [lastMessageNumber] + * @param {number} [firstMessageNumber] + * @param {number} [minTime] Unix timestamp lower bound + * @param {number} [maxTime] Unix timestamp upper bound + * @param {string} [msgId] Filter by stored message id + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + listMessagesRequestOpts(requestParameters: ListMessagesRequest): Promise; + + /** + * List stored chat history for this channel (requires history storage). Filter with `chatId`, `limit`, `last`, message numbers, or time bounds. + * @summary Get messages list + * @param {string} token JWT token or API key for authorization + * @param {string} [chatId] Filter messages for one chat (`phone@c.us`, group `@g.us`, or BSUID `@lid`) + * @param {number} [limit] Max rows to return (default 100) + * @param {boolean} [last] If true, return the latest messages (DESC). Default false. + * @param {number} [lastMessageNumber] + * @param {number} [firstMessageNumber] + * @param {number} [minTime] Unix timestamp lower bound + * @param {number} [maxTime] Unix timestamp upper bound + * @param {string} [msgId] Filter by stored message id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + listMessagesRaw(requestParameters: ListMessagesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * List stored chat history for this channel (requires history storage). Filter with `chatId`, `limit`, `last`, message numbers, or time bounds. + * Get messages list + */ + listMessages(token: string, chatId?: string, limit?: number, last?: boolean, lastMessageNumber?: number, firstMessageNumber?: number, minTime?: number, maxTime?: number, msgId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for retrieveMedia without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} mediaId + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + retrieveMediaRequestOpts(requestParameters: RetrieveMediaRequest): Promise; + + /** + * Get WABA media URL and metadata by mediaId (from uploadMedia). The returned `url` is temporary and typically expires within ~5 minutes. + * @summary Retrieve uploaded media metadata + * @param {string} token JWT token or API key for authorization + * @param {string} mediaId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + retrieveMediaRaw(requestParameters: RetrieveMediaRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Get WABA media URL and metadata by mediaId (from uploadMedia). The returned `url` is temporary and typically expires within ~5 minutes. + * Retrieve uploaded media metadata + */ + retrieveMedia(token: string, mediaId: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendAddressMessage without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendAddressMessageRequest} sendAddressMessageRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendAddressMessageRequestOpts(requestParameters: SendAddressMessageOperationRequest): Promise; + + /** + * Request shipping address from the user (WhatsApp interactive `address_message`). **India and Singapore only.** Requires: - Business WhatsApp number registered in that country - Recipient phone matching the country (`+91` ↔ `IN`, `+65` ↔ `SG`) Pass `country: \"IN\"` or `country: \"SG\"`. Eligibility is validated upstream; mismatches (e.g. Singapore phone with `country: \"IN\"`) return errors such as `Unsupported Interactive Message type` (HTTP 200 with `sent: false`). Optional action parameters: `values`, `saved_addresses`, `validation_errors`. + * @summary Send address request message + * @param {string} token JWT token or API key for authorization + * @param {SendAddressMessageRequest} sendAddressMessageRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendAddressMessageRaw(requestParameters: SendAddressMessageOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Request shipping address from the user (WhatsApp interactive `address_message`). **India and Singapore only.** Requires: - Business WhatsApp number registered in that country - Recipient phone matching the country (`+91` ↔ `IN`, `+65` ↔ `SG`) Pass `country: \"IN\"` or `country: \"SG\"`. Eligibility is validated upstream; mismatches (e.g. Singapore phone with `country: \"IN\"`) return errors such as `Unsupported Interactive Message type` (HTTP 200 with `sent: false`). Optional action parameters: `values`, `saved_addresses`, `validation_errors`. + * Send address request message + */ + sendAddressMessage(token: string, sendAddressMessageRequest: SendAddressMessageRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendButton without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendButtonRequest} [sendButtonRequest] + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendButtonRequestOpts(requestParameters: SendButtonOperationRequest): Promise; + + /** + * + * @summary Send Reply Buttons Message + * @param {string} token JWT token or API key for authorization + * @param {SendButtonRequest} [sendButtonRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendButtonRaw(requestParameters: SendButtonOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send Reply Buttons Message + */ + sendButton(token: string, sendButtonRequest?: SendButtonRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendCarousel without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} [body] Text shown above the carousel. Optional. If omitted and params include a body component, the body will be taken from params. + * @param {Array<{ [key: string]: any; }>} [cards] Preferred. 2–10 carousel cards (header + body + CTA or quick replies). + * @param {Array} [params] Required. Template-like structure (same as sendTemplate params). Must include a CAROUSEL component and its cards. Structure: - params[] item with type CAROUSEL and cards (2..10) - cards[] item: - components[] items: - header: parameters[0] with type image/video/product - image: {\\\"image\\\": {\\\"link\\\": \\\"https://...\\\"}} - video: {\\\"video\\\": {\\\"link\\\": \\\"https://...\\\"}} - product: {\\\"product\\\": {\\\"catalog_id\\\": \\\"...\\\", \\\"product_retailer_id\\\": \\\"...\\\"}} - button: sub_type url or quick_reply - url: parameters {\\\"text\\\": \\\"Button\\\", \\\"url\\\": \\\"https://...\\\"} - quick_reply: parameters[] of {\\\"id\\\": \\\"...\\\", \\\"text\\\": \\\"...\\\"} + * @param {string} [quotedMsgId] Quoted message ID (Cloud API) + * @param {string} [chatId] Required if phone is not set. Chat ID from the message list. Examples: 12020721369@c.us or 120363046942338209@g.us(group) + * @param {number} [phone] Required if chatId is not set. A phone number starting with the country code. USA example: 12020721369. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendCarouselRequestOpts(requestParameters: SendCarouselRequest): Promise; + + /** + * You can send product cards via Carousel in two ways: Template messages: do not require a 24-hour customer service window between you and the recipient. Use sendTemplate. Free-form messages: can be sent only when a customer service window is open between you and the recipient. Use sendCarousel. The message structure in /sendCarousel is largely similar to sending a template. However, in this case you must explicitly specify all elements that are created in advance when working with templates. This is because the message is sent without using a template. In /sendCarousel, for a Catalog Carousel there can be either 1 URL button or one or more quick reply buttons. Prefer the simple `cards` array (2–10 cards). Each card needs a header media/product plus a CTA URL or quick-reply buttons. Example: ```json { \"phone\": \"79001234567\", \"body\": \"Pick a product\", \"cards\": [ { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/1.jpg\" } }, \"body\": { \"text\": \"Card one\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p1\", \"display_text\": \"Open\" } } }, { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/2.jpg\" } }, \"body\": { \"text\": \"Card two\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p2\", \"display_text\": \"Open\" } } } ] } ``` `params` with a `CAROUSEL` component is the template-like alternative. + * @summary Send Carousel + * @param {string} token JWT token or API key for authorization + * @param {string} [body] Text shown above the carousel. Optional. If omitted and params include a body component, the body will be taken from params. + * @param {Array<{ [key: string]: any; }>} [cards] Preferred. 2–10 carousel cards (header + body + CTA or quick replies). + * @param {Array} [params] Required. Template-like structure (same as sendTemplate params). Must include a CAROUSEL component and its cards. Structure: - params[] item with type CAROUSEL and cards (2..10) - cards[] item: - components[] items: - header: parameters[0] with type image/video/product - image: {\\\"image\\\": {\\\"link\\\": \\\"https://...\\\"}} - video: {\\\"video\\\": {\\\"link\\\": \\\"https://...\\\"}} - product: {\\\"product\\\": {\\\"catalog_id\\\": \\\"...\\\", \\\"product_retailer_id\\\": \\\"...\\\"}} - button: sub_type url or quick_reply - url: parameters {\\\"text\\\": \\\"Button\\\", \\\"url\\\": \\\"https://...\\\"} - quick_reply: parameters[] of {\\\"id\\\": \\\"...\\\", \\\"text\\\": \\\"...\\\"} + * @param {string} [quotedMsgId] Quoted message ID (Cloud API) + * @param {string} [chatId] Required if phone is not set. Chat ID from the message list. Examples: 12020721369@c.us or 120363046942338209@g.us(group) + * @param {number} [phone] Required if chatId is not set. A phone number starting with the country code. USA example: 12020721369. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendCarouselRaw(requestParameters: SendCarouselRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * You can send product cards via Carousel in two ways: Template messages: do not require a 24-hour customer service window between you and the recipient. Use sendTemplate. Free-form messages: can be sent only when a customer service window is open between you and the recipient. Use sendCarousel. The message structure in /sendCarousel is largely similar to sending a template. However, in this case you must explicitly specify all elements that are created in advance when working with templates. This is because the message is sent without using a template. In /sendCarousel, for a Catalog Carousel there can be either 1 URL button or one or more quick reply buttons. Prefer the simple `cards` array (2–10 cards). Each card needs a header media/product plus a CTA URL or quick-reply buttons. Example: ```json { \"phone\": \"79001234567\", \"body\": \"Pick a product\", \"cards\": [ { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/1.jpg\" } }, \"body\": { \"text\": \"Card one\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p1\", \"display_text\": \"Open\" } } }, { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/2.jpg\" } }, \"body\": { \"text\": \"Card two\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p2\", \"display_text\": \"Open\" } } } ] } ``` `params` with a `CAROUSEL` component is the template-like alternative. + * Send Carousel + */ + sendCarousel(token: string, body?: string, cards?: Array<{ [key: string]: any; }>, params?: Array, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendContact without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendContactRequest} [sendContactRequest] + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendContactRequestOpts(requestParameters: SendContactOperationRequest): Promise; + + /** + * Send a WhatsApp contact card. Preferred simple body (legacy-compatible): ```json { \"phone\": \"79001234567\", \"contact\": { \"name\": \"Lida\", \"phone\": \"+79181976551\" } } ``` Cloud API shape also works: `contacts` is an array of objects with `name.formatted_name` and `phones[].phone` (E.164 string, **not** a timestamp). + * @summary Send a Contact + * @param {string} token JWT token or API key for authorization + * @param {SendContactRequest} [sendContactRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendContactRaw(requestParameters: SendContactOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a WhatsApp contact card. Preferred simple body (legacy-compatible): ```json { \"phone\": \"79001234567\", \"contact\": { \"name\": \"Lida\", \"phone\": \"+79181976551\" } } ``` Cloud API shape also works: `contacts` is an array of objects with `name.formatted_name` and `phones[].phone` (E.164 string, **not** a timestamp). + * Send a Contact + */ + sendContact(token: string, sendContactRequest?: SendContactRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendCtaUrl without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendCtaUrlRequest} sendCtaUrlRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendCtaUrlRequestOpts(requestParameters: SendCtaUrlOperationRequest): Promise; + + /** + * Send an interactive message with a single call-to-action URL button. + * @summary Send CTA URL interactive message + * @param {string} token JWT token or API key for authorization + * @param {SendCtaUrlRequest} sendCtaUrlRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendCtaUrlRaw(requestParameters: SendCtaUrlOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send an interactive message with a single call-to-action URL button. + * Send CTA URL interactive message + */ + sendCtaUrl(token: string, sendCtaUrlRequest: SendCtaUrlRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendFile without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} [body] File source (required unless mediaId is set): - HTTP/HTTPS URL - Base64 data URI + * @param {string} [filename] File name with extension (required with body, not with mediaId) + * @param {string} [mediaId] WABA media id from uploadMedia (alternative to body) + * @param {string} [mediaType] Required when using mediaId + * @param {boolean} [voice] Native WhatsApp voice note (audio/ogg only) + * @param {string} [caption] + * @param {string} [quotedMsgId] + * @param {string} [chatId] + * @param {number} [phone] + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendFileRequestOpts(requestParameters: SendFileRequest): Promise; + + /** + * Send a file to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * @summary Send a File + * @param {string} token JWT token or API key for authorization + * @param {string} [body] File source (required unless mediaId is set): - HTTP/HTTPS URL - Base64 data URI + * @param {string} [filename] File name with extension (required with body, not with mediaId) + * @param {string} [mediaId] WABA media id from uploadMedia (alternative to body) + * @param {string} [mediaType] Required when using mediaId + * @param {boolean} [voice] Native WhatsApp voice note (audio/ogg only) + * @param {string} [caption] + * @param {string} [quotedMsgId] + * @param {string} [chatId] + * @param {number} [phone] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendFileRaw(requestParameters: SendFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a file to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a File + */ + sendFile(token: string, body?: string, filename?: string, mediaId?: string, mediaType?: SendFileMediaTypeEnum, voice?: boolean, caption?: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendFlow without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} body Flow message body text + * @param {string} flowId Published Flow ID + * @param {string} flowToken Flow token generated by the business + * @param {string} flowCta CTA button text + * @param {SendFlowRequestHeader} [header] + * @param {string} [footer] Footer text + * @param {string} [flowAction] Flow action type + * @param {object} [flowActionPayload] Required for flowAction=navigate (screen is required). Ignored for data_exchange. If data is provided, it must be a non-empty object. + * @param {string} [flowMessageVersion] Flow message version (default \\\"3\\\") + * @param {string} [mode] Flow mode (draft or published). If omitted, provider default applies + * @param {object} [flowActionData] Shortcut for flowActionPayload.data (optional) + * @param {string} [flowActionScreen] Shortcut for flowActionPayload.screen (optional) + * @param {string} [quotedMsgId] Quoted message ID (Cloud API) + * @param {string} [chatId] Required if phone is not set. Chat ID from the message list. Examples: 12020721369@c.us or 120363046942338209@g.us(group) + * @param {number} [phone] Required if chatId is not set. A phone number starting with the country code. USA example: 12020721369. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendFlowRequestOpts(requestParameters: SendFlowRequest): Promise; + + /** + * Send Interactive WhatsApp Flow message to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. Use this method to send a published WhatsApp Flow as a service (interactive) message. If the 24-hour window is closed, send a template with a FLOW button via /sendTemplate. + * @summary Send WhatsApp Flow Message + * @param {string} token JWT token or API key for authorization + * @param {string} body Flow message body text + * @param {string} flowId Published Flow ID + * @param {string} flowToken Flow token generated by the business + * @param {string} flowCta CTA button text + * @param {SendFlowRequestHeader} [header] + * @param {string} [footer] Footer text + * @param {string} [flowAction] Flow action type + * @param {object} [flowActionPayload] Required for flowAction=navigate (screen is required). Ignored for data_exchange. If data is provided, it must be a non-empty object. + * @param {string} [flowMessageVersion] Flow message version (default \\\"3\\\") + * @param {string} [mode] Flow mode (draft or published). If omitted, provider default applies + * @param {object} [flowActionData] Shortcut for flowActionPayload.data (optional) + * @param {string} [flowActionScreen] Shortcut for flowActionPayload.screen (optional) + * @param {string} [quotedMsgId] Quoted message ID (Cloud API) + * @param {string} [chatId] Required if phone is not set. Chat ID from the message list. Examples: 12020721369@c.us or 120363046942338209@g.us(group) + * @param {number} [phone] Required if chatId is not set. A phone number starting with the country code. USA example: 12020721369. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendFlowRaw(requestParameters: SendFlowRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send Interactive WhatsApp Flow message to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. Use this method to send a published WhatsApp Flow as a service (interactive) message. If the 24-hour window is closed, send a template with a FLOW button via /sendTemplate. + * Send WhatsApp Flow Message + */ + sendFlow(token: string, body: string, flowId: string, flowToken: string, flowCta: string, header?: SendFlowRequestHeader, footer?: string, flowAction?: SendFlowFlowActionEnum, flowActionPayload?: object, flowMessageVersion?: string, mode?: SendFlowModeEnum, flowActionData?: object, flowActionScreen?: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendList without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendListRequest} [sendListRequest] + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendListRequestOpts(requestParameters: SendListOperationRequest): Promise; + + /** + * + * @summary Send List Message + * @param {string} token JWT token or API key for authorization + * @param {SendListRequest} [sendListRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendListRaw(requestParameters: SendListOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send List Message + */ + sendList(token: string, sendListRequest?: SendListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendLocation without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} lat Latitude of the location. Example: 45.018337 + * @param {string} lng Longitude of the location. Example: -73.968285 + * @param {string} [address] Address of the location. Only displayed if name is present. Example: 9766 Valley View St., New York, NY 10024 + * @param {string} [name] Name of the location. Example: Facebook HQ + * @param {string} [quotedMsgId] Quoted message ID (Cloud API) + * @param {string} [chatId] Required if phone is not set. Chat ID from the message list. Examples: 12020721369@c.us or 120363046942338209@g.us(group) + * @param {number} [phone] Required if chatId is not set. A phone number starting with the country code. USA example: 12020721369. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendLocationRequestOpts(requestParameters: SendLocationRequest): Promise; + + /** + * Send a location to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * @summary Send a Location + * @param {string} token JWT token or API key for authorization + * @param {string} lat Latitude of the location. Example: 45.018337 + * @param {string} lng Longitude of the location. Example: -73.968285 + * @param {string} [address] Address of the location. Only displayed if name is present. Example: 9766 Valley View St., New York, NY 10024 + * @param {string} [name] Name of the location. Example: Facebook HQ + * @param {string} [quotedMsgId] Quoted message ID (Cloud API) + * @param {string} [chatId] Required if phone is not set. Chat ID from the message list. Examples: 12020721369@c.us or 120363046942338209@g.us(group) + * @param {number} [phone] Required if chatId is not set. A phone number starting with the country code. USA example: 12020721369. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendLocationRaw(requestParameters: SendLocationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a location to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a Location + */ + sendLocation(token: string, lat: string, lng: string, address?: string, name?: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendLocationRequest without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendLocationRequestRequest} [sendLocationRequestRequest] + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendLocationRequestRequestOpts(requestParameters: SendLocationRequestOperationRequest): Promise; + + /** + * + * @summary Send Location Request Message + * @param {string} token JWT token or API key for authorization + * @param {SendLocationRequestRequest} [sendLocationRequestRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendLocationRequestRaw(requestParameters: SendLocationRequestOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send Location Request Message + */ + sendLocationRequest(token: string, sendLocationRequestRequest?: SendLocationRequestRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendMessage without sending the request + * @param {string} token JWT token or API key for authorization + * @param {string} body Message text, UTF-8 or UTF-16 string with emoji + * @param {string} [quotedMsgId] Message ID to quote/reply to (Cloud API wamid) + * @param {string} [chatId] Recipient chat ID: phone@c.us, group@g.us, or BSUID@lid + * @param {number} [phone] Phone number starting with country code (alternative to chatId) + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendMessageRequestOpts(requestParameters: SendMessageRequest): Promise; + + /** + * Send a message to an existing chat. (Only if the dialogue has an Open Session). The message will be added to the queue for sending and delivered even if the phone is disconnected from the Internet or authorization is not passed. Only one of two parameters is needed to determine the destination - chatId or phone. + * @summary Send a Message + * @param {string} token JWT token or API key for authorization + * @param {string} body Message text, UTF-8 or UTF-16 string with emoji + * @param {string} [quotedMsgId] Message ID to quote/reply to (Cloud API wamid) + * @param {string} [chatId] Recipient chat ID: phone@c.us, group@g.us, or BSUID@lid + * @param {number} [phone] Phone number starting with country code (alternative to chatId) + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendMessageRaw(requestParameters: SendMessageRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a message to an existing chat. (Only if the dialogue has an Open Session). The message will be added to the queue for sending and delivered even if the phone is disconnected from the Internet or authorization is not passed. Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a Message + */ + sendMessage(token: string, body: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendOrderDetails without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendOrderDetailsRequest} sendOrderDetailsRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendOrderDetailsRequestOpts(requestParameters: SendOrderDetailsOperationRequest): Promise; + + /** + * Send a WhatsApp **order details** payment / invoice message using a pre-approved **Utility** template that has an `ORDER_DETAILS` button. **India only** (WhatsApp Payments India). Requires: - India WhatsApp Business number - Commerce enabled on the channel (`GET`/`POST /commerce`) - Approved template with an `ORDER_DETAILS` button Use this method when you need structured fields (`order`, `referenceId`, `currency`, `paymentSettings`). The API appends a template button `sub_type: order_details` and sends via the same path as `POST /sendTemplate`. Works **outside the 24-hour session window** (template message). You can also send the same payload yourself with `POST /sendTemplate` by including a button component in `params`: ```json { \"type\": \"button\", \"sub_type\": \"order_details\", \"index\": 0, \"parameters\": [{ \"type\": \"action\", \"action\": { \"order_details\": { \"reference_id\": \"order-123\", \"currency\": \"INR\", \"order\": { \"status\": \"pending\", \"items\": [], \"subtotal\": { \"offset\": 100, \"value\": 50000 } } } } }] } ``` + * @summary Send order details (India payments template) + * @param {string} token JWT token or API key for authorization + * @param {SendOrderDetailsRequest} sendOrderDetailsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendOrderDetailsRaw(requestParameters: SendOrderDetailsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a WhatsApp **order details** payment / invoice message using a pre-approved **Utility** template that has an `ORDER_DETAILS` button. **India only** (WhatsApp Payments India). Requires: - India WhatsApp Business number - Commerce enabled on the channel (`GET`/`POST /commerce`) - Approved template with an `ORDER_DETAILS` button Use this method when you need structured fields (`order`, `referenceId`, `currency`, `paymentSettings`). The API appends a template button `sub_type: order_details` and sends via the same path as `POST /sendTemplate`. Works **outside the 24-hour session window** (template message). You can also send the same payload yourself with `POST /sendTemplate` by including a button component in `params`: ```json { \"type\": \"button\", \"sub_type\": \"order_details\", \"index\": 0, \"parameters\": [{ \"type\": \"action\", \"action\": { \"order_details\": { \"reference_id\": \"order-123\", \"currency\": \"INR\", \"order\": { \"status\": \"pending\", \"items\": [], \"subtotal\": { \"offset\": 100, \"value\": 50000 } } } } }] } ``` + * Send order details (India payments template) + */ + sendOrderDetails(token: string, sendOrderDetailsRequest: SendOrderDetailsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendPaymentRequest without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendPaymentRequestRequest} sendPaymentRequestRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendPaymentRequestRequestOpts(requestParameters: SendPaymentRequestOperationRequest): Promise; + + /** + * Send a regional payment request interactive message (beta scaffold). `region` must be IN, SG, or BR. Payload shape follows Meta regional payments docs; verify on stage before production use. Full regional builders are not implemented yet. + * @summary Send payment request (regional) + * @param {string} token JWT token or API key for authorization + * @param {SendPaymentRequestRequest} sendPaymentRequestRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendPaymentRequestRaw(requestParameters: SendPaymentRequestOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a regional payment request interactive message (beta scaffold). `region` must be IN, SG, or BR. Payload shape follows Meta regional payments docs; verify on stage before production use. Full regional builders are not implemented yet. + * Send payment request (regional) + */ + sendPaymentRequest(token: string, sendPaymentRequestRequest: SendPaymentRequestRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendProduct without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendProductRequest} sendProductRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendProductRequestOpts(requestParameters: SendProductOperationRequest): Promise; + + /** + * Send a single catalog product or a product list. Destination is `chatId` or `phone`. `action` is required: - single product: `{ \"catalog_id\": \"...\", \"product_retailer_id\": \"...\" }` - product list: `{ \"catalog_id\": \"...\", \"sections\": [{ \"title\": \"...\", \"product_items\": [{ \"product_retailer_id\": \"...\" }] }] }` + * @summary Send a Product + * @param {string} token JWT token or API key for authorization + * @param {SendProductRequest} sendProductRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendProductRaw(requestParameters: SendProductOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a single catalog product or a product list. Destination is `chatId` or `phone`. `action` is required: - single product: `{ \"catalog_id\": \"...\", \"product_retailer_id\": \"...\" }` - product list: `{ \"catalog_id\": \"...\", \"sections\": [{ \"title\": \"...\", \"product_items\": [{ \"product_retailer_id\": \"...\" }] }] }` + * Send a Product + */ + sendProduct(token: string, sendProductRequest: SendProductRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendReaction without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendReactionRequest} [sendReactionRequest] + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendReactionRequestOpts(requestParameters: SendReactionOperationRequest): Promise; + + /** + * + * @summary Send Reaction + * @param {string} token JWT token or API key for authorization + * @param {SendReactionRequest} [sendReactionRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendReactionRaw(requestParameters: SendReactionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send Reaction + */ + sendReaction(token: string, sendReactionRequest?: SendReactionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for sendSticker without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendStickerRequest} sendStickerRequest + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendStickerRequestOpts(requestParameters: SendStickerOperationRequest): Promise; + + /** + * Send a WhatsApp sticker by mediaId or link URL. + * @summary Send sticker message + * @param {string} token JWT token or API key for authorization + * @param {SendStickerRequest} sendStickerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MessagingApiInterface + */ + sendStickerRaw(requestParameters: SendStickerOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a WhatsApp sticker by mediaId or link URL. + * Send sticker message + */ + sendSticker(token: string, sendStickerRequest: SendStickerRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + +} + +/** + * + */ +export class MessagingApi extends runtime.BaseAPI implements MessagingApiInterface { + + /** + * Creates request options for createReadMessage without sending the request + */ + async createReadMessageRequestOpts(requestParameters: CreateReadMessageRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createReadMessage().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + { contentType: 'application/json' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['messageId'] != null) { + formParams.append('messageId', requestParameters['messageId'] as any); + } + + if (requestParameters['msgId'] != null) { + formParams.append('msgId', requestParameters['msgId'] as any); + } + + if (requestParameters['typingIndicator'] != null) { + formParams.append('typingIndicator', requestParameters['typingIndicator'] as any); + } + + + let urlPath = `/readMessage`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + * Mark message as read + */ + async createReadMessageRaw(requestParameters: CreateReadMessageRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createReadMessageRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateReadMessage200ResponseFromJSON(jsonValue)); + } + + /** + * Mark message as read + */ + async createReadMessage(token: string, messageId?: string, msgId?: string, typingIndicator?: boolean, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createReadMessageRaw({ token: token, messageId: messageId, msgId: msgId, typingIndicator: typingIndicator }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for createUploadMedia without sending the request + */ + async createUploadMediaRequestOpts(requestParameters: CreateUploadMediaOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling createUploadMedia().' + ); + } + + if (requestParameters['createUploadMediaRequest'] == null) { + throw new runtime.RequiredError( + 'createUploadMediaRequest', + 'Required parameter "createUploadMediaRequest" was null or undefined when calling createUploadMedia().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/uploadMedia`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CreateUploadMediaRequestToJSON(requestParameters['createUploadMediaRequest']), + }; + } + + /** + * Upload a file to WhatsApp media storage and get a numeric `mediaId` for later use with `send_file` / `send_sticker` (`mediaId` field, not `body`). Pass the file as `body` **or** `url` — an HTTP(S) URL or a base64 data URI. The `mediaId` in the response is a numeric string (keep it as a string). + * Upload media + */ + async createUploadMediaRaw(requestParameters: CreateUploadMediaOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createUploadMediaRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CreateUploadMedia200ResponseFromJSON(jsonValue)); + } + + /** + * Upload a file to WhatsApp media storage and get a numeric `mediaId` for later use with `send_file` / `send_sticker` (`mediaId` field, not `body`). Pass the file as `body` **or** `url` — an HTTP(S) URL or a base64 data URI. The `mediaId` in the response is a numeric string (keep it as a string). + * Upload media + */ + async createUploadMedia(token: string, createUploadMediaRequest: CreateUploadMediaRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createUploadMediaRaw({ token: token, createUploadMediaRequest: createUploadMediaRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for deleteMedia without sending the request + */ + async deleteMediaRequestOpts(requestParameters: DeleteMediaRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling deleteMedia().' + ); + } + + if (requestParameters['mediaId'] == null) { + throw new runtime.RequiredError( + 'mediaId', + 'Required parameter "mediaId" was null or undefined when calling deleteMedia().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/media/{mediaId}`; + urlPath = urlPath.replace('{mediaId}', encodeURIComponent(String(requestParameters['mediaId']))); + + return { + path: urlPath, + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Delete previously uploaded media by numeric `mediaId` (from `/uploadMedia`). This is the canonical deletion endpoint and uses the REST `DELETE` verb on the media resource path. The older `POST /deleteMedia` is a deprecated alias. + * Delete media from WABA storage + */ + async deleteMediaRaw(requestParameters: DeleteMediaRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.deleteMediaRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseFromJSON(jsonValue)); + } + + /** + * Delete previously uploaded media by numeric `mediaId` (from `/uploadMedia`). This is the canonical deletion endpoint and uses the REST `DELETE` verb on the media resource path. The older `POST /deleteMedia` is a deprecated alias. + * Delete media from WABA storage + */ + async deleteMedia(token: string, mediaId: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.deleteMediaRaw({ token: token, mediaId: mediaId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for deleteMediaLegacy without sending the request + * @deprecated + */ + async deleteMediaLegacyRequestOpts(requestParameters: DeleteMediaLegacyOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling deleteMediaLegacy().' + ); + } + + if (requestParameters['deleteMediaLegacyRequest'] == null) { + throw new runtime.RequiredError( + 'deleteMediaLegacyRequest', + 'Required parameter "deleteMediaLegacyRequest" was null or undefined when calling deleteMediaLegacy().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/deleteMedia`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: DeleteMediaLegacyRequestToJSON(requestParameters['deleteMediaLegacyRequest']), + }; + } + + /** + * **Deprecated.** Use `DELETE /media/{mediaId}` instead. This POST alias is kept for backward compatibility with earlier integrations. New integrations should call `DELETE /media/{mediaId}`: 1msg follows REST conventions for resource deletion going forward (delete a resource with the `DELETE` verb on its resource path). + * Delete media from WABA storage (deprecated alias) + * @deprecated + */ + async deleteMediaLegacyRaw(requestParameters: DeleteMediaLegacyOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.deleteMediaLegacyRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseFromJSON(jsonValue)); + } + + /** + * **Deprecated.** Use `DELETE /media/{mediaId}` instead. This POST alias is kept for backward compatibility with earlier integrations. New integrations should call `DELETE /media/{mediaId}`: 1msg follows REST conventions for resource deletion going forward (delete a resource with the `DELETE` verb on its resource path). + * Delete media from WABA storage (deprecated alias) + * @deprecated + */ + async deleteMediaLegacy(token: string, deleteMediaLegacyRequest: DeleteMediaLegacyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.deleteMediaLegacyRaw({ token: token, deleteMediaLegacyRequest: deleteMediaLegacyRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for getMmLiteStatus without sending the request + */ + async getMmLiteStatusRequestOpts(requestParameters: GetMmLiteStatusRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getMmLiteStatus().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/mmLiteStatus`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get MM Lite availability and status + */ + async getMmLiteStatusRaw(requestParameters: GetMmLiteStatusRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getMmLiteStatusRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => GetMmLiteStatus200ResponseFromJSON(jsonValue)); + } + + /** + * Get MM Lite availability and status + */ + async getMmLiteStatus(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getMmLiteStatusRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listMessages without sending the request + */ + async listMessagesRequestOpts(requestParameters: ListMessagesRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling listMessages().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['chatId'] != null) { + queryParameters['chatId'] = requestParameters['chatId']; + } + + if (requestParameters['limit'] != null) { + queryParameters['limit'] = requestParameters['limit']; + } + + if (requestParameters['last'] != null) { + queryParameters['last'] = requestParameters['last']; + } + + if (requestParameters['lastMessageNumber'] != null) { + queryParameters['lastMessageNumber'] = requestParameters['lastMessageNumber']; + } + + if (requestParameters['firstMessageNumber'] != null) { + queryParameters['firstMessageNumber'] = requestParameters['firstMessageNumber']; + } + + if (requestParameters['minTime'] != null) { + queryParameters['min_time'] = requestParameters['minTime']; + } + + if (requestParameters['maxTime'] != null) { + queryParameters['max_time'] = requestParameters['maxTime']; + } + + if (requestParameters['msgId'] != null) { + queryParameters['msgId'] = requestParameters['msgId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/messages`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * List stored chat history for this channel (requires history storage). Filter with `chatId`, `limit`, `last`, message numbers, or time bounds. + * Get messages list + */ + async listMessagesRaw(requestParameters: ListMessagesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.listMessagesRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ListMessages200ResponseFromJSON(jsonValue)); + } + + /** + * List stored chat history for this channel (requires history storage). Filter with `chatId`, `limit`, `last`, message numbers, or time bounds. + * Get messages list + */ + async listMessages(token: string, chatId?: string, limit?: number, last?: boolean, lastMessageNumber?: number, firstMessageNumber?: number, minTime?: number, maxTime?: number, msgId?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listMessagesRaw({ token: token, chatId: chatId, limit: limit, last: last, lastMessageNumber: lastMessageNumber, firstMessageNumber: firstMessageNumber, minTime: minTime, maxTime: maxTime, msgId: msgId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for retrieveMedia without sending the request + */ + async retrieveMediaRequestOpts(requestParameters: RetrieveMediaRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling retrieveMedia().' + ); + } + + if (requestParameters['mediaId'] == null) { + throw new runtime.RequiredError( + 'mediaId', + 'Required parameter "mediaId" was null or undefined when calling retrieveMedia().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['mediaId'] != null) { + queryParameters['mediaId'] = requestParameters['mediaId']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/retrieveMedia`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get WABA media URL and metadata by mediaId (from uploadMedia). The returned `url` is temporary and typically expires within ~5 minutes. + * Retrieve uploaded media metadata + */ + async retrieveMediaRaw(requestParameters: RetrieveMediaRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.retrieveMediaRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => RetrieveMedia200ResponseFromJSON(jsonValue)); + } + + /** + * Get WABA media URL and metadata by mediaId (from uploadMedia). The returned `url` is temporary and typically expires within ~5 minutes. + * Retrieve uploaded media metadata + */ + async retrieveMedia(token: string, mediaId: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.retrieveMediaRaw({ token: token, mediaId: mediaId }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendAddressMessage without sending the request + */ + async sendAddressMessageRequestOpts(requestParameters: SendAddressMessageOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendAddressMessage().' + ); + } + + if (requestParameters['sendAddressMessageRequest'] == null) { + throw new runtime.RequiredError( + 'sendAddressMessageRequest', + 'Required parameter "sendAddressMessageRequest" was null or undefined when calling sendAddressMessage().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendAddressMessage`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendAddressMessageRequestToJSON(requestParameters['sendAddressMessageRequest']), + }; + } + + /** + * Request shipping address from the user (WhatsApp interactive `address_message`). **India and Singapore only.** Requires: - Business WhatsApp number registered in that country - Recipient phone matching the country (`+91` ↔ `IN`, `+65` ↔ `SG`) Pass `country: \"IN\"` or `country: \"SG\"`. Eligibility is validated upstream; mismatches (e.g. Singapore phone with `country: \"IN\"`) return errors such as `Unsupported Interactive Message type` (HTTP 200 with `sent: false`). Optional action parameters: `values`, `saved_addresses`, `validation_errors`. + * Send address request message + */ + async sendAddressMessageRaw(requestParameters: SendAddressMessageOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendAddressMessageRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Request shipping address from the user (WhatsApp interactive `address_message`). **India and Singapore only.** Requires: - Business WhatsApp number registered in that country - Recipient phone matching the country (`+91` ↔ `IN`, `+65` ↔ `SG`) Pass `country: \"IN\"` or `country: \"SG\"`. Eligibility is validated upstream; mismatches (e.g. Singapore phone with `country: \"IN\"`) return errors such as `Unsupported Interactive Message type` (HTTP 200 with `sent: false`). Optional action parameters: `values`, `saved_addresses`, `validation_errors`. + * Send address request message + */ + async sendAddressMessage(token: string, sendAddressMessageRequest: SendAddressMessageRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendAddressMessageRaw({ token: token, sendAddressMessageRequest: sendAddressMessageRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendButton without sending the request + */ + async sendButtonRequestOpts(requestParameters: SendButtonOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendButton().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendButton`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendButtonRequestToJSON(requestParameters['sendButtonRequest']), + }; + } + + /** + * Send Reply Buttons Message + */ + async sendButtonRaw(requestParameters: SendButtonOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendButtonRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send Reply Buttons Message + */ + async sendButton(token: string, sendButtonRequest?: SendButtonRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendButtonRaw({ token: token, sendButtonRequest: sendButtonRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendCarousel without sending the request + */ + async sendCarouselRequestOpts(requestParameters: SendCarouselRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendCarousel().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + { contentType: 'application/json' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['body'] != null) { + formParams.append('body', requestParameters['body'] as any); + } + + if (requestParameters['cards'] != null) { + formParams.append('cards', requestParameters['cards']!.join(runtime.COLLECTION_FORMATS["csv"])); + } + + if (requestParameters['params'] != null) { + formParams.append('params', requestParameters['params']!.join(runtime.COLLECTION_FORMATS["csv"])); + } + + if (requestParameters['quotedMsgId'] != null) { + formParams.append('quotedMsgId', requestParameters['quotedMsgId'] as any); + } + + if (requestParameters['chatId'] != null) { + formParams.append('chatId', requestParameters['chatId'] as any); + } + + if (requestParameters['phone'] != null) { + formParams.append('phone', requestParameters['phone'] as any); + } + + + let urlPath = `/sendCarousel`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + * You can send product cards via Carousel in two ways: Template messages: do not require a 24-hour customer service window between you and the recipient. Use sendTemplate. Free-form messages: can be sent only when a customer service window is open between you and the recipient. Use sendCarousel. The message structure in /sendCarousel is largely similar to sending a template. However, in this case you must explicitly specify all elements that are created in advance when working with templates. This is because the message is sent without using a template. In /sendCarousel, for a Catalog Carousel there can be either 1 URL button or one or more quick reply buttons. Prefer the simple `cards` array (2–10 cards). Each card needs a header media/product plus a CTA URL or quick-reply buttons. Example: ```json { \"phone\": \"79001234567\", \"body\": \"Pick a product\", \"cards\": [ { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/1.jpg\" } }, \"body\": { \"text\": \"Card one\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p1\", \"display_text\": \"Open\" } } }, { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/2.jpg\" } }, \"body\": { \"text\": \"Card two\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p2\", \"display_text\": \"Open\" } } } ] } ``` `params` with a `CAROUSEL` component is the template-like alternative. + * Send Carousel + */ + async sendCarouselRaw(requestParameters: SendCarouselRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendCarouselRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * You can send product cards via Carousel in two ways: Template messages: do not require a 24-hour customer service window between you and the recipient. Use sendTemplate. Free-form messages: can be sent only when a customer service window is open between you and the recipient. Use sendCarousel. The message structure in /sendCarousel is largely similar to sending a template. However, in this case you must explicitly specify all elements that are created in advance when working with templates. This is because the message is sent without using a template. In /sendCarousel, for a Catalog Carousel there can be either 1 URL button or one or more quick reply buttons. Prefer the simple `cards` array (2–10 cards). Each card needs a header media/product plus a CTA URL or quick-reply buttons. Example: ```json { \"phone\": \"79001234567\", \"body\": \"Pick a product\", \"cards\": [ { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/1.jpg\" } }, \"body\": { \"text\": \"Card one\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p1\", \"display_text\": \"Open\" } } }, { \"header\": { \"type\": \"image\", \"image\": { \"link\": \"https://example.com/2.jpg\" } }, \"body\": { \"text\": \"Card two\" }, \"type\": \"cta_url\", \"action\": { \"name\": \"cta_url\", \"parameters\": { \"url\": \"https://example.com/p2\", \"display_text\": \"Open\" } } } ] } ``` `params` with a `CAROUSEL` component is the template-like alternative. + * Send Carousel + */ + async sendCarousel(token: string, body?: string, cards?: Array<{ [key: string]: any; }>, params?: Array, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendCarouselRaw({ token: token, body: body, cards: cards, params: params, quotedMsgId: quotedMsgId, chatId: chatId, phone: phone }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendContact without sending the request + */ + async sendContactRequestOpts(requestParameters: SendContactOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendContact().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendContact`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendContactRequestToJSON(requestParameters['sendContactRequest']), + }; + } + + /** + * Send a WhatsApp contact card. Preferred simple body (legacy-compatible): ```json { \"phone\": \"79001234567\", \"contact\": { \"name\": \"Lida\", \"phone\": \"+79181976551\" } } ``` Cloud API shape also works: `contacts` is an array of objects with `name.formatted_name` and `phones[].phone` (E.164 string, **not** a timestamp). + * Send a Contact + */ + async sendContactRaw(requestParameters: SendContactOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendContactRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a WhatsApp contact card. Preferred simple body (legacy-compatible): ```json { \"phone\": \"79001234567\", \"contact\": { \"name\": \"Lida\", \"phone\": \"+79181976551\" } } ``` Cloud API shape also works: `contacts` is an array of objects with `name.formatted_name` and `phones[].phone` (E.164 string, **not** a timestamp). + * Send a Contact + */ + async sendContact(token: string, sendContactRequest?: SendContactRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendContactRaw({ token: token, sendContactRequest: sendContactRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendCtaUrl without sending the request + */ + async sendCtaUrlRequestOpts(requestParameters: SendCtaUrlOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendCtaUrl().' + ); + } + + if (requestParameters['sendCtaUrlRequest'] == null) { + throw new runtime.RequiredError( + 'sendCtaUrlRequest', + 'Required parameter "sendCtaUrlRequest" was null or undefined when calling sendCtaUrl().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendCtaUrl`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendCtaUrlRequestToJSON(requestParameters['sendCtaUrlRequest']), + }; + } + + /** + * Send an interactive message with a single call-to-action URL button. + * Send CTA URL interactive message + */ + async sendCtaUrlRaw(requestParameters: SendCtaUrlOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendCtaUrlRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send an interactive message with a single call-to-action URL button. + * Send CTA URL interactive message + */ + async sendCtaUrl(token: string, sendCtaUrlRequest: SendCtaUrlRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendCtaUrlRaw({ token: token, sendCtaUrlRequest: sendCtaUrlRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendFile without sending the request + */ + async sendFileRequestOpts(requestParameters: SendFileRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendFile().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + { contentType: 'application/json' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['body'] != null) { + formParams.append('body', requestParameters['body'] as any); + } + + if (requestParameters['filename'] != null) { + formParams.append('filename', requestParameters['filename'] as any); + } + + if (requestParameters['mediaId'] != null) { + formParams.append('mediaId', requestParameters['mediaId'] as any); + } + + if (requestParameters['mediaType'] != null) { + formParams.append('mediaType', requestParameters['mediaType'] as any); + } + + if (requestParameters['voice'] != null) { + formParams.append('voice', requestParameters['voice'] as any); + } + + if (requestParameters['caption'] != null) { + formParams.append('caption', requestParameters['caption'] as any); + } + + if (requestParameters['quotedMsgId'] != null) { + formParams.append('quotedMsgId', requestParameters['quotedMsgId'] as any); + } + + if (requestParameters['chatId'] != null) { + formParams.append('chatId', requestParameters['chatId'] as any); + } + + if (requestParameters['phone'] != null) { + formParams.append('phone', requestParameters['phone'] as any); + } + + + let urlPath = `/sendFile`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + * Send a file to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a File + */ + async sendFileRaw(requestParameters: SendFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendFileRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a file to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a File + */ + async sendFile(token: string, body?: string, filename?: string, mediaId?: string, mediaType?: SendFileMediaTypeEnum, voice?: boolean, caption?: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendFileRaw({ token: token, body: body, filename: filename, mediaId: mediaId, mediaType: mediaType, voice: voice, caption: caption, quotedMsgId: quotedMsgId, chatId: chatId, phone: phone }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendFlow without sending the request + */ + async sendFlowRequestOpts(requestParameters: SendFlowRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendFlow().' + ); + } + + if (requestParameters['body'] == null) { + throw new runtime.RequiredError( + 'body', + 'Required parameter "body" was null or undefined when calling sendFlow().' + ); + } + + if (requestParameters['flowId'] == null) { + throw new runtime.RequiredError( + 'flowId', + 'Required parameter "flowId" was null or undefined when calling sendFlow().' + ); + } + + if (requestParameters['flowToken'] == null) { + throw new runtime.RequiredError( + 'flowToken', + 'Required parameter "flowToken" was null or undefined when calling sendFlow().' + ); + } + + if (requestParameters['flowCta'] == null) { + throw new runtime.RequiredError( + 'flowCta', + 'Required parameter "flowCta" was null or undefined when calling sendFlow().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + { contentType: 'application/json' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['body'] != null) { + formParams.append('body', requestParameters['body'] as any); + } + + if (requestParameters['header'] != null) { + formParams.append('header', new Blob([JSON.stringify(SendFlowRequestHeaderToJSON(requestParameters['header']))], { type: "application/json", })); + } + + if (requestParameters['footer'] != null) { + formParams.append('footer', requestParameters['footer'] as any); + } + + if (requestParameters['flowId'] != null) { + formParams.append('flowId', requestParameters['flowId'] as any); + } + + if (requestParameters['flowToken'] != null) { + formParams.append('flowToken', requestParameters['flowToken'] as any); + } + + if (requestParameters['flowCta'] != null) { + formParams.append('flowCta', requestParameters['flowCta'] as any); + } + + if (requestParameters['flowAction'] != null) { + formParams.append('flowAction', requestParameters['flowAction'] as any); + } + + if (requestParameters['flowActionPayload'] != null) { + formParams.append('flowActionPayload', new Blob([JSON.stringify(objectToJSON(requestParameters['flowActionPayload']))], { type: "application/json", })); + } + + if (requestParameters['flowMessageVersion'] != null) { + formParams.append('flowMessageVersion', requestParameters['flowMessageVersion'] as any); + } + + if (requestParameters['mode'] != null) { + formParams.append('mode', requestParameters['mode'] as any); + } + + if (requestParameters['flowActionData'] != null) { + formParams.append('flowActionData', new Blob([JSON.stringify(objectToJSON(requestParameters['flowActionData']))], { type: "application/json", })); + } + + if (requestParameters['flowActionScreen'] != null) { + formParams.append('flowActionScreen', requestParameters['flowActionScreen'] as any); + } + + if (requestParameters['quotedMsgId'] != null) { + formParams.append('quotedMsgId', requestParameters['quotedMsgId'] as any); + } + + if (requestParameters['chatId'] != null) { + formParams.append('chatId', requestParameters['chatId'] as any); + } + + if (requestParameters['phone'] != null) { + formParams.append('phone', requestParameters['phone'] as any); + } + + + let urlPath = `/sendFlow`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + * Send Interactive WhatsApp Flow message to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. Use this method to send a published WhatsApp Flow as a service (interactive) message. If the 24-hour window is closed, send a template with a FLOW button via /sendTemplate. + * Send WhatsApp Flow Message + */ + async sendFlowRaw(requestParameters: SendFlowRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendFlowRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send Interactive WhatsApp Flow message to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. Use this method to send a published WhatsApp Flow as a service (interactive) message. If the 24-hour window is closed, send a template with a FLOW button via /sendTemplate. + * Send WhatsApp Flow Message + */ + async sendFlow(token: string, body: string, flowId: string, flowToken: string, flowCta: string, header?: SendFlowRequestHeader, footer?: string, flowAction?: SendFlowFlowActionEnum, flowActionPayload?: object, flowMessageVersion?: string, mode?: SendFlowModeEnum, flowActionData?: object, flowActionScreen?: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendFlowRaw({ token: token, body: body, flowId: flowId, flowToken: flowToken, flowCta: flowCta, header: header, footer: footer, flowAction: flowAction, flowActionPayload: flowActionPayload, flowMessageVersion: flowMessageVersion, mode: mode, flowActionData: flowActionData, flowActionScreen: flowActionScreen, quotedMsgId: quotedMsgId, chatId: chatId, phone: phone }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendList without sending the request + */ + async sendListRequestOpts(requestParameters: SendListOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendList().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendList`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendListRequestToJSON(requestParameters['sendListRequest']), + }; + } + + /** + * Send List Message + */ + async sendListRaw(requestParameters: SendListOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendListRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send List Message + */ + async sendList(token: string, sendListRequest?: SendListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendListRaw({ token: token, sendListRequest: sendListRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendLocation without sending the request + */ + async sendLocationRequestOpts(requestParameters: SendLocationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendLocation().' + ); + } + + if (requestParameters['lat'] == null) { + throw new runtime.RequiredError( + 'lat', + 'Required parameter "lat" was null or undefined when calling sendLocation().' + ); + } + + if (requestParameters['lng'] == null) { + throw new runtime.RequiredError( + 'lng', + 'Required parameter "lng" was null or undefined when calling sendLocation().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + { contentType: 'application/json' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['lat'] != null) { + formParams.append('lat', requestParameters['lat'] as any); + } + + if (requestParameters['lng'] != null) { + formParams.append('lng', requestParameters['lng'] as any); + } + + if (requestParameters['address'] != null) { + formParams.append('address', requestParameters['address'] as any); + } + + if (requestParameters['name'] != null) { + formParams.append('name', requestParameters['name'] as any); + } + + if (requestParameters['quotedMsgId'] != null) { + formParams.append('quotedMsgId', requestParameters['quotedMsgId'] as any); + } + + if (requestParameters['chatId'] != null) { + formParams.append('chatId', requestParameters['chatId'] as any); + } + + if (requestParameters['phone'] != null) { + formParams.append('phone', requestParameters['phone'] as any); + } + + + let urlPath = `/sendLocation`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + * Send a location to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a Location + */ + async sendLocationRaw(requestParameters: SendLocationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendLocationRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a location to an existing chat. (Only if the dialogue has an Open Session). Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a Location + */ + async sendLocation(token: string, lat: string, lng: string, address?: string, name?: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendLocationRaw({ token: token, lat: lat, lng: lng, address: address, name: name, quotedMsgId: quotedMsgId, chatId: chatId, phone: phone }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendLocationRequest without sending the request + */ + async sendLocationRequestRequestOpts(requestParameters: SendLocationRequestOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendLocationRequest().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendLocationRequest`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendLocationRequestRequestToJSON(requestParameters['sendLocationRequestRequest']), + }; + } + + /** + * Send Location Request Message + */ + async sendLocationRequestRaw(requestParameters: SendLocationRequestOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendLocationRequestRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send Location Request Message + */ + async sendLocationRequest(token: string, sendLocationRequestRequest?: SendLocationRequestRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendLocationRequestRaw({ token: token, sendLocationRequestRequest: sendLocationRequestRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendMessage without sending the request + */ + async sendMessageRequestOpts(requestParameters: SendMessageRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendMessage().' + ); + } + + if (requestParameters['body'] == null) { + throw new runtime.RequiredError( + 'body', + 'Required parameter "body" was null or undefined when calling sendMessage().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + { contentType: 'application/json' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['body'] != null) { + formParams.append('body', requestParameters['body'] as any); + } + + if (requestParameters['quotedMsgId'] != null) { + formParams.append('quotedMsgId', requestParameters['quotedMsgId'] as any); + } + + if (requestParameters['chatId'] != null) { + formParams.append('chatId', requestParameters['chatId'] as any); + } + + if (requestParameters['phone'] != null) { + formParams.append('phone', requestParameters['phone'] as any); + } + + + let urlPath = `/sendMessage`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + * Send a message to an existing chat. (Only if the dialogue has an Open Session). The message will be added to the queue for sending and delivered even if the phone is disconnected from the Internet or authorization is not passed. Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a Message + */ + async sendMessageRaw(requestParameters: SendMessageRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendMessageRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a message to an existing chat. (Only if the dialogue has an Open Session). The message will be added to the queue for sending and delivered even if the phone is disconnected from the Internet or authorization is not passed. Only one of two parameters is needed to determine the destination - chatId or phone. + * Send a Message + */ + async sendMessage(token: string, body: string, quotedMsgId?: string, chatId?: string, phone?: number, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendMessageRaw({ token: token, body: body, quotedMsgId: quotedMsgId, chatId: chatId, phone: phone }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendOrderDetails without sending the request + */ + async sendOrderDetailsRequestOpts(requestParameters: SendOrderDetailsOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendOrderDetails().' + ); + } + + if (requestParameters['sendOrderDetailsRequest'] == null) { + throw new runtime.RequiredError( + 'sendOrderDetailsRequest', + 'Required parameter "sendOrderDetailsRequest" was null or undefined when calling sendOrderDetails().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendOrderDetails`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendOrderDetailsRequestToJSON(requestParameters['sendOrderDetailsRequest']), + }; + } + + /** + * Send a WhatsApp **order details** payment / invoice message using a pre-approved **Utility** template that has an `ORDER_DETAILS` button. **India only** (WhatsApp Payments India). Requires: - India WhatsApp Business number - Commerce enabled on the channel (`GET`/`POST /commerce`) - Approved template with an `ORDER_DETAILS` button Use this method when you need structured fields (`order`, `referenceId`, `currency`, `paymentSettings`). The API appends a template button `sub_type: order_details` and sends via the same path as `POST /sendTemplate`. Works **outside the 24-hour session window** (template message). You can also send the same payload yourself with `POST /sendTemplate` by including a button component in `params`: ```json { \"type\": \"button\", \"sub_type\": \"order_details\", \"index\": 0, \"parameters\": [{ \"type\": \"action\", \"action\": { \"order_details\": { \"reference_id\": \"order-123\", \"currency\": \"INR\", \"order\": { \"status\": \"pending\", \"items\": [], \"subtotal\": { \"offset\": 100, \"value\": 50000 } } } } }] } ``` + * Send order details (India payments template) + */ + async sendOrderDetailsRaw(requestParameters: SendOrderDetailsOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendOrderDetailsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a WhatsApp **order details** payment / invoice message using a pre-approved **Utility** template that has an `ORDER_DETAILS` button. **India only** (WhatsApp Payments India). Requires: - India WhatsApp Business number - Commerce enabled on the channel (`GET`/`POST /commerce`) - Approved template with an `ORDER_DETAILS` button Use this method when you need structured fields (`order`, `referenceId`, `currency`, `paymentSettings`). The API appends a template button `sub_type: order_details` and sends via the same path as `POST /sendTemplate`. Works **outside the 24-hour session window** (template message). You can also send the same payload yourself with `POST /sendTemplate` by including a button component in `params`: ```json { \"type\": \"button\", \"sub_type\": \"order_details\", \"index\": 0, \"parameters\": [{ \"type\": \"action\", \"action\": { \"order_details\": { \"reference_id\": \"order-123\", \"currency\": \"INR\", \"order\": { \"status\": \"pending\", \"items\": [], \"subtotal\": { \"offset\": 100, \"value\": 50000 } } } } }] } ``` + * Send order details (India payments template) + */ + async sendOrderDetails(token: string, sendOrderDetailsRequest: SendOrderDetailsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendOrderDetailsRaw({ token: token, sendOrderDetailsRequest: sendOrderDetailsRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendPaymentRequest without sending the request + */ + async sendPaymentRequestRequestOpts(requestParameters: SendPaymentRequestOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendPaymentRequest().' + ); + } + + if (requestParameters['sendPaymentRequestRequest'] == null) { + throw new runtime.RequiredError( + 'sendPaymentRequestRequest', + 'Required parameter "sendPaymentRequestRequest" was null or undefined when calling sendPaymentRequest().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendPaymentRequest`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendPaymentRequestRequestToJSON(requestParameters['sendPaymentRequestRequest']), + }; + } + + /** + * Send a regional payment request interactive message (beta scaffold). `region` must be IN, SG, or BR. Payload shape follows Meta regional payments docs; verify on stage before production use. Full regional builders are not implemented yet. + * Send payment request (regional) + */ + async sendPaymentRequestRaw(requestParameters: SendPaymentRequestOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendPaymentRequestRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a regional payment request interactive message (beta scaffold). `region` must be IN, SG, or BR. Payload shape follows Meta regional payments docs; verify on stage before production use. Full regional builders are not implemented yet. + * Send payment request (regional) + */ + async sendPaymentRequest(token: string, sendPaymentRequestRequest: SendPaymentRequestRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendPaymentRequestRaw({ token: token, sendPaymentRequestRequest: sendPaymentRequestRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendProduct without sending the request + */ + async sendProductRequestOpts(requestParameters: SendProductOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendProduct().' + ); + } + + if (requestParameters['sendProductRequest'] == null) { + throw new runtime.RequiredError( + 'sendProductRequest', + 'Required parameter "sendProductRequest" was null or undefined when calling sendProduct().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendProduct`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendProductRequestToJSON(requestParameters['sendProductRequest']), + }; + } + + /** + * Send a single catalog product or a product list. Destination is `chatId` or `phone`. `action` is required: - single product: `{ \"catalog_id\": \"...\", \"product_retailer_id\": \"...\" }` - product list: `{ \"catalog_id\": \"...\", \"sections\": [{ \"title\": \"...\", \"product_items\": [{ \"product_retailer_id\": \"...\" }] }] }` + * Send a Product + */ + async sendProductRaw(requestParameters: SendProductOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendProductRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a single catalog product or a product list. Destination is `chatId` or `phone`. `action` is required: - single product: `{ \"catalog_id\": \"...\", \"product_retailer_id\": \"...\" }` - product list: `{ \"catalog_id\": \"...\", \"sections\": [{ \"title\": \"...\", \"product_items\": [{ \"product_retailer_id\": \"...\" }] }] }` + * Send a Product + */ + async sendProduct(token: string, sendProductRequest: SendProductRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendProductRaw({ token: token, sendProductRequest: sendProductRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendReaction without sending the request + */ + async sendReactionRequestOpts(requestParameters: SendReactionOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendReaction().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendReaction`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendReactionRequestToJSON(requestParameters['sendReactionRequest']), + }; + } + + /** + * Send Reaction + */ + async sendReactionRaw(requestParameters: SendReactionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendReactionRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send Reaction + */ + async sendReaction(token: string, sendReactionRequest?: SendReactionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendReactionRaw({ token: token, sendReactionRequest: sendReactionRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendSticker without sending the request + */ + async sendStickerRequestOpts(requestParameters: SendStickerOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendSticker().' + ); + } + + if (requestParameters['sendStickerRequest'] == null) { + throw new runtime.RequiredError( + 'sendStickerRequest', + 'Required parameter "sendStickerRequest" was null or undefined when calling sendSticker().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendSticker`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendStickerRequestToJSON(requestParameters['sendStickerRequest']), + }; + } + + /** + * Send a WhatsApp sticker by mediaId or link URL. + * Send sticker message + */ + async sendStickerRaw(requestParameters: SendStickerOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendStickerRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => MessageSentResponseFromJSON(jsonValue)); + } + + /** + * Send a WhatsApp sticker by mediaId or link URL. + * Send sticker message + */ + async sendSticker(token: string, sendStickerRequest: SendStickerRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.sendStickerRaw({ token: token, sendStickerRequest: sendStickerRequest }, initOverrides); + return await response.value(); + } + +} + +/** + * @export + */ +export const SendFileMediaTypeEnum = { + Image: 'image', + Video: 'video', + Audio: 'audio', + Document: 'document' +} as const; +export type SendFileMediaTypeEnum = typeof SendFileMediaTypeEnum[keyof typeof SendFileMediaTypeEnum]; +/** + * @export + */ +export const SendFlowFlowActionEnum = { + Navigate: 'navigate', + DataExchange: 'data_exchange' +} as const; +export type SendFlowFlowActionEnum = typeof SendFlowFlowActionEnum[keyof typeof SendFlowFlowActionEnum]; +/** + * @export + */ +export const SendFlowModeEnum = { + Draft: 'draft', + Published: 'published' +} as const; +export type SendFlowModeEnum = typeof SendFlowModeEnum[keyof typeof SendFlowModeEnum]; diff --git a/quality/mcp/vendor/apis/ProfileApi.ts b/quality/mcp/vendor/apis/ProfileApi.ts new file mode 100644 index 0000000..dddf5e4 --- /dev/null +++ b/quality/mcp/vendor/apis/ProfileApi.ts @@ -0,0 +1,228 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type ProfileInfo, + ProfileInfoFromJSON, + ProfileInfoToJSON, +} from '../models/ProfileInfo'; +import { + type UpdateMe200Response, + UpdateMe200ResponseFromJSON, + UpdateMe200ResponseToJSON, +} from '../models/UpdateMe200Response'; +import { + type UpdateMeRequest, + UpdateMeRequestFromJSON, + UpdateMeRequestToJSON, +} from '../models/UpdateMeRequest'; + +export interface GetMeRequest { + token: string; +} + +export interface UpdateMeOperationRequest { + token: string; + updateMeRequest: UpdateMeRequest; +} + +/** + * ProfileApi - interface + * + * @export + * @interface ProfileApiInterface + */ +export interface ProfileApiInterface { + /** + * Creates request options for getMe without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof ProfileApiInterface + */ + getMeRequestOpts(requestParameters: GetMeRequest): Promise; + + /** + * Return the WhatsApp Business profile for this channel: About, address, description, email, phone, websites, industry (`vertical`), and profile photo URL. + * @summary Get Profile Info + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProfileApiInterface + */ + getMeRaw(requestParameters: GetMeRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Return the WhatsApp Business profile for this channel: About, address, description, email, phone, websites, industry (`vertical`), and profile photo URL. + * Get Profile Info + */ + getMe(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for updateMe without sending the request + * @param {string} token JWT token or API key for authorization + * @param {UpdateMeRequest} updateMeRequest + * @throws {RequiredError} + * @memberof ProfileApiInterface + */ + updateMeRequestOpts(requestParameters: UpdateMeOperationRequest): Promise; + + /** + * Update WhatsApp Business Account profile fields. At least one of about, description, email, photo, address, vertical, websites is required. Blocked when the channel subscription limit is exceeded. + * @summary Update profile info + * @param {string} token JWT token or API key for authorization + * @param {UpdateMeRequest} updateMeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ProfileApiInterface + */ + updateMeRaw(requestParameters: UpdateMeOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Update WhatsApp Business Account profile fields. At least one of about, description, email, photo, address, vertical, websites is required. Blocked when the channel subscription limit is exceeded. + * Update profile info + */ + updateMe(token: string, updateMeRequest: UpdateMeRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + +} + +/** + * + */ +export class ProfileApi extends runtime.BaseAPI implements ProfileApiInterface { + + /** + * Creates request options for getMe without sending the request + */ + async getMeRequestOpts(requestParameters: GetMeRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getMe().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/me`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Return the WhatsApp Business profile for this channel: About, address, description, email, phone, websites, industry (`vertical`), and profile photo URL. + * Get Profile Info + */ + async getMeRaw(requestParameters: GetMeRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getMeRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ProfileInfoFromJSON(jsonValue)); + } + + /** + * Return the WhatsApp Business profile for this channel: About, address, description, email, phone, websites, industry (`vertical`), and profile photo URL. + * Get Profile Info + */ + async getMe(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getMeRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for updateMe without sending the request + */ + async updateMeRequestOpts(requestParameters: UpdateMeOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling updateMe().' + ); + } + + if (requestParameters['updateMeRequest'] == null) { + throw new runtime.RequiredError( + 'updateMeRequest', + 'Required parameter "updateMeRequest" was null or undefined when calling updateMe().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/me`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: UpdateMeRequestToJSON(requestParameters['updateMeRequest']), + }; + } + + /** + * Update WhatsApp Business Account profile fields. At least one of about, description, email, photo, address, vertical, websites is required. Blocked when the channel subscription limit is exceeded. + * Update profile info + */ + async updateMeRaw(requestParameters: UpdateMeOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.updateMeRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => UpdateMe200ResponseFromJSON(jsonValue)); + } + + /** + * Update WhatsApp Business Account profile fields. At least one of about, description, email, photo, address, vertical, websites is required. Blocked when the channel subscription limit is exceeded. + * Update profile info + */ + async updateMe(token: string, updateMeRequest: UpdateMeRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.updateMeRaw({ token: token, updateMeRequest: updateMeRequest }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/TemplatesApi.ts b/quality/mcp/vendor/apis/TemplatesApi.ts new file mode 100644 index 0000000..fbe4d95 --- /dev/null +++ b/quality/mcp/vendor/apis/TemplatesApi.ts @@ -0,0 +1,445 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type AddTemplate200Response, + AddTemplate200ResponseFromJSON, + AddTemplate200ResponseToJSON, +} from '../models/AddTemplate200Response'; +import { + type AddTemplateRequest, + AddTemplateRequestFromJSON, + AddTemplateRequestToJSON, +} from '../models/AddTemplateRequest'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type ListTemplates200Response, + ListTemplates200ResponseFromJSON, + ListTemplates200ResponseToJSON, +} from '../models/ListTemplates200Response'; +import { + type RemoveTemplateRequest, + RemoveTemplateRequestFromJSON, + RemoveTemplateRequestToJSON, +} from '../models/RemoveTemplateRequest'; +import { + type SendTemplateRequest, + SendTemplateRequestFromJSON, + SendTemplateRequestToJSON, +} from '../models/SendTemplateRequest'; + +export interface AddTemplateOperationRequest { + token: string; + addTemplateRequest?: AddTemplateRequest; +} + +export interface ListTemplatesRequest { + token: string; + limit?: number; + offset?: number; + sort?: ListTemplatesSortEnum; +} + +export interface RemoveTemplateOperationRequest { + token: string; + removeTemplateRequest: RemoveTemplateRequest; +} + +export interface SendTemplateOperationRequest { + token: string; + sendTemplateRequest?: SendTemplateRequest; +} + +/** + * TemplatesApi - interface + * + * @export + * @interface TemplatesApiInterface + */ +export interface TemplatesApiInterface { + /** + * Creates request options for addTemplate without sending the request + * @param {string} token JWT token or API key for authorization + * @param {AddTemplateRequest} [addTemplateRequest] + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + addTemplateRequestOpts(requestParameters: AddTemplateOperationRequest): Promise; + + /** + * Create a WABA message template (`name`, `category`, `language`, `components`). After create, Meta reviews the template; list it with `GET /templates`. + * @summary Create message template + * @param {string} token JWT token or API key for authorization + * @param {AddTemplateRequest} [addTemplateRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + addTemplateRaw(requestParameters: AddTemplateOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Create a WABA message template (`name`, `category`, `language`, `components`). After create, Meta reviews the template; list it with `GET /templates`. + * Create message template + */ + addTemplate(token: string, addTemplateRequest?: AddTemplateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for listTemplates without sending the request + * @param {string} token JWT token or API key for authorization + * @param {number} [limit] + * @param {number} [offset] + * @param {'id' | 'name' | 'status'} [sort] Sort field (`id`, `name`, or `status`) + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + listTemplatesRequestOpts(requestParameters: ListTemplatesRequest): Promise; + + /** + * List message templates. Paginate with `limit` / `offset`; sort by `id`, `name`, or `status`. + * @summary Get templates list + * @param {string} token JWT token or API key for authorization + * @param {number} [limit] + * @param {number} [offset] + * @param {'id' | 'name' | 'status'} [sort] Sort field (`id`, `name`, or `status`) + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + listTemplatesRaw(requestParameters: ListTemplatesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * List message templates. Paginate with `limit` / `offset`; sort by `id`, `name`, or `status`. + * Get templates list + */ + listTemplates(token: string, limit?: number, offset?: number, sort?: ListTemplatesSortEnum, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for removeTemplate without sending the request + * @param {string} token JWT token or API key for authorization + * @param {RemoveTemplateRequest} removeTemplateRequest + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + removeTemplateRequestOpts(requestParameters: RemoveTemplateOperationRequest): Promise; + + /** + * Delete a message template by `name` (all language versions of that name). + * @summary Remove message template + * @param {string} token JWT token or API key for authorization + * @param {RemoveTemplateRequest} removeTemplateRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + removeTemplateRaw(requestParameters: RemoveTemplateOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Delete a message template by `name` (all language versions of that name). + * Remove message template + */ + removeTemplate(token: string, removeTemplateRequest: RemoveTemplateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + + /** + * Creates request options for sendTemplate without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SendTemplateRequest} [sendTemplateRequest] + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + sendTemplateRequestOpts(requestParameters: SendTemplateOperationRequest): Promise; + + /** + * Send a WhatsApp **template** message (works outside the 24-hour session window). Use this tool — not `send_message` — when the 24h session is closed. `params` MUST be Cloud API **component objects**, never a flat list of strings and never the old On-Prem `hsm` / `localizable_params` format. Example body variable `{{1}}`: ```json { \"type\": \"body\", \"parameters\": [{ \"type\": \"text\", \"text\": \"Ivan\" }] } ``` Do not send `\"type\": \"hsm\"` and do not send `params: [\"Ivan\", \"123\"]`. Supported `params` button `sub_type` values include: `url`, `quick_reply`, `copy_code` / `coupon_code`, `catalog`, `flow`, `limited_time_offer`, and **`order_details`** (WhatsApp Payments **India only** — requires an approved Utility template with an ORDER_DETAILS button). Convenience wrapper with structured fields: `POST /sendOrderDetails`. + * @summary Send Template Message + * @param {string} token JWT token or API key for authorization + * @param {SendTemplateRequest} [sendTemplateRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApiInterface + */ + sendTemplateRaw(requestParameters: SendTemplateOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Send a WhatsApp **template** message (works outside the 24-hour session window). Use this tool — not `send_message` — when the 24h session is closed. `params` MUST be Cloud API **component objects**, never a flat list of strings and never the old On-Prem `hsm` / `localizable_params` format. Example body variable `{{1}}`: ```json { \"type\": \"body\", \"parameters\": [{ \"type\": \"text\", \"text\": \"Ivan\" }] } ``` Do not send `\"type\": \"hsm\"` and do not send `params: [\"Ivan\", \"123\"]`. Supported `params` button `sub_type` values include: `url`, `quick_reply`, `copy_code` / `coupon_code`, `catalog`, `flow`, `limited_time_offer`, and **`order_details`** (WhatsApp Payments **India only** — requires an approved Utility template with an ORDER_DETAILS button). Convenience wrapper with structured fields: `POST /sendOrderDetails`. + * Send Template Message + */ + sendTemplate(token: string, sendTemplateRequest?: SendTemplateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }>; + +} + +/** + * + */ +export class TemplatesApi extends runtime.BaseAPI implements TemplatesApiInterface { + + /** + * Creates request options for addTemplate without sending the request + */ + async addTemplateRequestOpts(requestParameters: AddTemplateOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling addTemplate().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/addTemplate`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: AddTemplateRequestToJSON(requestParameters['addTemplateRequest']), + }; + } + + /** + * Create a WABA message template (`name`, `category`, `language`, `components`). After create, Meta reviews the template; list it with `GET /templates`. + * Create message template + */ + async addTemplateRaw(requestParameters: AddTemplateOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.addTemplateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AddTemplate200ResponseFromJSON(jsonValue)); + } + + /** + * Create a WABA message template (`name`, `category`, `language`, `components`). After create, Meta reviews the template; list it with `GET /templates`. + * Create message template + */ + async addTemplate(token: string, addTemplateRequest?: AddTemplateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.addTemplateRaw({ token: token, addTemplateRequest: addTemplateRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listTemplates without sending the request + */ + async listTemplatesRequestOpts(requestParameters: ListTemplatesRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling listTemplates().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + if (requestParameters['limit'] != null) { + queryParameters['limit'] = requestParameters['limit']; + } + + if (requestParameters['offset'] != null) { + queryParameters['offset'] = requestParameters['offset']; + } + + if (requestParameters['sort'] != null) { + queryParameters['sort'] = requestParameters['sort']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/templates`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * List message templates. Paginate with `limit` / `offset`; sort by `id`, `name`, or `status`. + * Get templates list + */ + async listTemplatesRaw(requestParameters: ListTemplatesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.listTemplatesRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ListTemplates200ResponseFromJSON(jsonValue)); + } + + /** + * List message templates. Paginate with `limit` / `offset`; sort by `id`, `name`, or `status`. + * Get templates list + */ + async listTemplates(token: string, limit?: number, offset?: number, sort?: ListTemplatesSortEnum, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listTemplatesRaw({ token: token, limit: limit, offset: offset, sort: sort }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for removeTemplate without sending the request + */ + async removeTemplateRequestOpts(requestParameters: RemoveTemplateOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling removeTemplate().' + ); + } + + if (requestParameters['removeTemplateRequest'] == null) { + throw new runtime.RequiredError( + 'removeTemplateRequest', + 'Required parameter "removeTemplateRequest" was null or undefined when calling removeTemplate().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/removeTemplate`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: RemoveTemplateRequestToJSON(requestParameters['removeTemplateRequest']), + }; + } + + /** + * Delete a message template by `name` (all language versions of that name). + * Remove message template + */ + async removeTemplateRaw(requestParameters: RemoveTemplateOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.removeTemplateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Delete a message template by `name` (all language versions of that name). + * Remove message template + */ + async removeTemplate(token: string, removeTemplateRequest: RemoveTemplateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.removeTemplateRaw({ token: token, removeTemplateRequest: removeTemplateRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for sendTemplate without sending the request + */ + async sendTemplateRequestOpts(requestParameters: SendTemplateOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling sendTemplate().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/sendTemplate`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SendTemplateRequestToJSON(requestParameters['sendTemplateRequest']), + }; + } + + /** + * Send a WhatsApp **template** message (works outside the 24-hour session window). Use this tool — not `send_message` — when the 24h session is closed. `params` MUST be Cloud API **component objects**, never a flat list of strings and never the old On-Prem `hsm` / `localizable_params` format. Example body variable `{{1}}`: ```json { \"type\": \"body\", \"parameters\": [{ \"type\": \"text\", \"text\": \"Ivan\" }] } ``` Do not send `\"type\": \"hsm\"` and do not send `params: [\"Ivan\", \"123\"]`. Supported `params` button `sub_type` values include: `url`, `quick_reply`, `copy_code` / `coupon_code`, `catalog`, `flow`, `limited_time_offer`, and **`order_details`** (WhatsApp Payments **India only** — requires an approved Utility template with an ORDER_DETAILS button). Convenience wrapper with structured fields: `POST /sendOrderDetails`. + * Send Template Message + */ + async sendTemplateRaw(requestParameters: SendTemplateOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.sendTemplateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Send a WhatsApp **template** message (works outside the 24-hour session window). Use this tool — not `send_message` — when the 24h session is closed. `params` MUST be Cloud API **component objects**, never a flat list of strings and never the old On-Prem `hsm` / `localizable_params` format. Example body variable `{{1}}`: ```json { \"type\": \"body\", \"parameters\": [{ \"type\": \"text\", \"text\": \"Ivan\" }] } ``` Do not send `\"type\": \"hsm\"` and do not send `params: [\"Ivan\", \"123\"]`. Supported `params` button `sub_type` values include: `url`, `quick_reply`, `copy_code` / `coupon_code`, `catalog`, `flow`, `limited_time_offer`, and **`order_details`** (WhatsApp Payments **India only** — requires an approved Utility template with an ORDER_DETAILS button). Convenience wrapper with structured fields: `POST /sendOrderDetails`. + * Send Template Message + */ + async sendTemplate(token: string, sendTemplateRequest?: SendTemplateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{ [key: string]: any; }> { + const response = await this.sendTemplateRaw({ token: token, sendTemplateRequest: sendTemplateRequest }, initOverrides); + return await response.value(); + } + +} + +/** + * @export + */ +export const ListTemplatesSortEnum = { + Id: 'id', + Name: 'name', + Status: 'status' +} as const; +export type ListTemplatesSortEnum = typeof ListTemplatesSortEnum[keyof typeof ListTemplatesSortEnum]; diff --git a/quality/mcp/vendor/apis/UsersApi.ts b/quality/mcp/vendor/apis/UsersApi.ts new file mode 100644 index 0000000..1e45232 --- /dev/null +++ b/quality/mcp/vendor/apis/UsersApi.ts @@ -0,0 +1,317 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type BlockUserRequest, + BlockUserRequestFromJSON, + BlockUserRequestToJSON, +} from '../models/BlockUserRequest'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type ListBlockedUsers200Response, + ListBlockedUsers200ResponseFromJSON, + ListBlockedUsers200ResponseToJSON, +} from '../models/ListBlockedUsers200Response'; +import { + type SuccessResponse, + SuccessResponseFromJSON, + SuccessResponseToJSON, +} from '../models/SuccessResponse'; + +export interface BlockUserOperationRequest { + token: string; + blockUserRequest: BlockUserRequest; +} + +export interface ListBlockedUsersRequest { + token: string; +} + +export interface UnblockUserRequest { + token: string; + blockUserRequest: BlockUserRequest; +} + +/** + * UsersApi - interface + * + * @export + * @interface UsersApiInterface + */ +export interface UsersApiInterface { + /** + * Creates request options for blockUser without sending the request + * @param {string} token JWT token or API key for authorization + * @param {BlockUserRequest} blockUserRequest + * @throws {RequiredError} + * @memberof UsersApiInterface + */ + blockUserRequestOpts(requestParameters: BlockUserOperationRequest): Promise; + + /** + * + * @summary Block WhatsApp user + * @param {string} token JWT token or API key for authorization + * @param {BlockUserRequest} blockUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApiInterface + */ + blockUserRaw(requestParameters: BlockUserOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Block WhatsApp user + */ + blockUser(token: string, blockUserRequest: BlockUserRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for listBlockedUsers without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof UsersApiInterface + */ + listBlockedUsersRequestOpts(requestParameters: ListBlockedUsersRequest): Promise; + + /** + * Returns users currently blocked on this WhatsApp channel (WABA `GET /block_users`). Same channel token auth as `blockUser` / `unblockUser`. + * @summary List blocked WhatsApp users + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApiInterface + */ + listBlockedUsersRaw(requestParameters: ListBlockedUsersRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Returns users currently blocked on this WhatsApp channel (WABA `GET /block_users`). Same channel token auth as `blockUser` / `unblockUser`. + * List blocked WhatsApp users + */ + listBlockedUsers(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for unblockUser without sending the request + * @param {string} token JWT token or API key for authorization + * @param {BlockUserRequest} blockUserRequest + * @throws {RequiredError} + * @memberof UsersApiInterface + */ + unblockUserRequestOpts(requestParameters: UnblockUserRequest): Promise; + + /** + * + * @summary Unblock WhatsApp user + * @param {string} token JWT token or API key for authorization + * @param {BlockUserRequest} blockUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApiInterface + */ + unblockUserRaw(requestParameters: UnblockUserRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Unblock WhatsApp user + */ + unblockUser(token: string, blockUserRequest: BlockUserRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + +} + +/** + * + */ +export class UsersApi extends runtime.BaseAPI implements UsersApiInterface { + + /** + * Creates request options for blockUser without sending the request + */ + async blockUserRequestOpts(requestParameters: BlockUserOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling blockUser().' + ); + } + + if (requestParameters['blockUserRequest'] == null) { + throw new runtime.RequiredError( + 'blockUserRequest', + 'Required parameter "blockUserRequest" was null or undefined when calling blockUser().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/blockUser`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: BlockUserRequestToJSON(requestParameters['blockUserRequest']), + }; + } + + /** + * Block WhatsApp user + */ + async blockUserRaw(requestParameters: BlockUserOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.blockUserRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseFromJSON(jsonValue)); + } + + /** + * Block WhatsApp user + */ + async blockUser(token: string, blockUserRequest: BlockUserRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.blockUserRaw({ token: token, blockUserRequest: blockUserRequest }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listBlockedUsers without sending the request + */ + async listBlockedUsersRequestOpts(requestParameters: ListBlockedUsersRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling listBlockedUsers().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/blockedUsers`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Returns users currently blocked on this WhatsApp channel (WABA `GET /block_users`). Same channel token auth as `blockUser` / `unblockUser`. + * List blocked WhatsApp users + */ + async listBlockedUsersRaw(requestParameters: ListBlockedUsersRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.listBlockedUsersRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ListBlockedUsers200ResponseFromJSON(jsonValue)); + } + + /** + * Returns users currently blocked on this WhatsApp channel (WABA `GET /block_users`). Same channel token auth as `blockUser` / `unblockUser`. + * List blocked WhatsApp users + */ + async listBlockedUsers(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listBlockedUsersRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for unblockUser without sending the request + */ + async unblockUserRequestOpts(requestParameters: UnblockUserRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling unblockUser().' + ); + } + + if (requestParameters['blockUserRequest'] == null) { + throw new runtime.RequiredError( + 'blockUserRequest', + 'Required parameter "blockUserRequest" was null or undefined when calling unblockUser().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/unblockUser`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: BlockUserRequestToJSON(requestParameters['blockUserRequest']), + }; + } + + /** + * Unblock WhatsApp user + */ + async unblockUserRaw(requestParameters: UnblockUserRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.unblockUserRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SuccessResponseFromJSON(jsonValue)); + } + + /** + * Unblock WhatsApp user + */ + async unblockUser(token: string, blockUserRequest: BlockUserRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.unblockUserRaw({ token: token, blockUserRequest: blockUserRequest }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/apis/WebhooksApi.ts b/quality/mcp/vendor/apis/WebhooksApi.ts new file mode 100644 index 0000000..5a1cea4 --- /dev/null +++ b/quality/mcp/vendor/apis/WebhooksApi.ts @@ -0,0 +1,228 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * 1MSG WhatsApp Business API (Public) + * Public client API for sending messages, managing groups, flows, and templates. This is the API exposed to end customers via platform.1msg.io **Authentication:** Pass the channel token in **one** place. The API checks JSON body (`token`), then query string (`?token=`), then `Authorization: Bearer` header (preferred in production). **Documentation:** https://help.1msg.io/platform-1msg/getting-start/quick-start **API Docs:** https://docs.1msg.io/ + * + * The version of the OpenAPI document: 1.0.0 + * Contact: support@1msg.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type ErrorResponse, + ErrorResponseFromJSON, + ErrorResponseToJSON, +} from '../models/ErrorResponse'; +import { + type GetWebhook200Response, + GetWebhook200ResponseFromJSON, + GetWebhook200ResponseToJSON, +} from '../models/GetWebhook200Response'; +import { + type SetWebhook200Response, + SetWebhook200ResponseFromJSON, + SetWebhook200ResponseToJSON, +} from '../models/SetWebhook200Response'; +import { + type SetWebhookRequest, + SetWebhookRequestFromJSON, + SetWebhookRequestToJSON, +} from '../models/SetWebhookRequest'; + +export interface GetWebhookRequest { + token: string; +} + +export interface SetWebhookOperationRequest { + token: string; + setWebhookRequest: SetWebhookRequest; +} + +/** + * WebhooksApi - interface + * + * @export + * @interface WebhooksApiInterface + */ +export interface WebhooksApiInterface { + /** + * Creates request options for getWebhook without sending the request + * @param {string} token JWT token or API key for authorization + * @throws {RequiredError} + * @memberof WebhooksApiInterface + */ + getWebhookRequestOpts(requestParameters: GetWebhookRequest): Promise; + + /** + * Return the webhook URL(s) currently configured for this channel, plus delivery status for each URL. `webhookUrl` is a string when a single URL is set, an array of strings when multiple URLs are configured, or `null` if none is set. **Prefer `webhookStatuses`.** The circuit breaker is per URL, not per channel. If 1 of 4 URLs is sticky-disabled, that URL is skipped; the other three still receive events. Send/receive are not affected. Channel-level `haveDisabledWebhooks`, `circuitState`, `reason`, `fails`, `trips`, and timestamps are a compatibility summary of **one** URL (the first sticky-disabled URL, else the first configured URL). They are not \"the channel is dead\". - `haveDisabledWebhooks` — `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled. Live URLs still get events. - `circuitState` — compressed scalar for that same primary URL: `ok` | `degraded` | `open` | `half_open` | `disabled`. - `disabledWebhooks` — currently configured URLs that are sticky-disabled (subset of `webhookUrl`). - `webhookStatuses` — one row per currently configured URL. Use this array to see which endpoints are live vs skipped. - `fails` / `trips` / `firstFailAt` — **diagnostics for support**, not product UI. `fails` = failures in a rolling ~10 minute window (not lifetime). `trips` = failed recovery probes on the 15 min → 1 h → 6 h schedule (sticky disable after 3). Clients can ignore these and use `webhookStatuses` + `disabledWebhooks` only. Sticky disable skips delivery; it does not unconfigure the URL (it stays in `webhookUrl`). Re-enable with `POST /webhook` or `POST /settings`. Those endpoints **replace** the whole list — with multiple URLs, send the full array. + * @summary Get webhook URL + * @param {string} token JWT token or API key for authorization + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WebhooksApiInterface + */ + getWebhookRaw(requestParameters: GetWebhookRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Return the webhook URL(s) currently configured for this channel, plus delivery status for each URL. `webhookUrl` is a string when a single URL is set, an array of strings when multiple URLs are configured, or `null` if none is set. **Prefer `webhookStatuses`.** The circuit breaker is per URL, not per channel. If 1 of 4 URLs is sticky-disabled, that URL is skipped; the other three still receive events. Send/receive are not affected. Channel-level `haveDisabledWebhooks`, `circuitState`, `reason`, `fails`, `trips`, and timestamps are a compatibility summary of **one** URL (the first sticky-disabled URL, else the first configured URL). They are not \"the channel is dead\". - `haveDisabledWebhooks` — `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled. Live URLs still get events. - `circuitState` — compressed scalar for that same primary URL: `ok` | `degraded` | `open` | `half_open` | `disabled`. - `disabledWebhooks` — currently configured URLs that are sticky-disabled (subset of `webhookUrl`). - `webhookStatuses` — one row per currently configured URL. Use this array to see which endpoints are live vs skipped. - `fails` / `trips` / `firstFailAt` — **diagnostics for support**, not product UI. `fails` = failures in a rolling ~10 minute window (not lifetime). `trips` = failed recovery probes on the 15 min → 1 h → 6 h schedule (sticky disable after 3). Clients can ignore these and use `webhookStatuses` + `disabledWebhooks` only. Sticky disable skips delivery; it does not unconfigure the URL (it stays in `webhookUrl`). Re-enable with `POST /webhook` or `POST /settings`. Those endpoints **replace** the whole list — with multiple URLs, send the full array. + * Get webhook URL + */ + getWebhook(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + + /** + * Creates request options for setWebhook without sending the request + * @param {string} token JWT token or API key for authorization + * @param {SetWebhookRequest} setWebhookRequest + * @throws {RequiredError} + * @memberof WebhooksApiInterface + */ + setWebhookRequestOpts(requestParameters: SetWebhookOperationRequest): Promise; + + /** + * Set the client webhook URL that receives inbound events for this channel. **Replaces** the stored webhook list (it does not append). To deliver the same events to several endpoints, pass an array in `webhookUrl` (max 5) or use `POST /settings`. With several URLs already configured, posting only the disabled URL **drops the others** — send the complete array to re-enable one of many. Incoming payloads include messages, delivery ACKs, group updates, coexistence echoes, and calling events. WhatsApp Calling payloads (`type: \"calls\"`) are documented under the **Calling** tag. Call permission replies arrive on the normal messages path (`call_permission_reply`). Repeating `POST /webhook` with the same URL(s) clears an auto-disable (self-serve re-enable). Send/receive are not affected by webhook auto-disable. + * @summary Set webhook URL + * @param {string} token JWT token or API key for authorization + * @param {SetWebhookRequest} setWebhookRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WebhooksApiInterface + */ + setWebhookRaw(requestParameters: SetWebhookOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Set the client webhook URL that receives inbound events for this channel. **Replaces** the stored webhook list (it does not append). To deliver the same events to several endpoints, pass an array in `webhookUrl` (max 5) or use `POST /settings`. With several URLs already configured, posting only the disabled URL **drops the others** — send the complete array to re-enable one of many. Incoming payloads include messages, delivery ACKs, group updates, coexistence echoes, and calling events. WhatsApp Calling payloads (`type: \"calls\"`) are documented under the **Calling** tag. Call permission replies arrive on the normal messages path (`call_permission_reply`). Repeating `POST /webhook` with the same URL(s) clears an auto-disable (self-serve re-enable). Send/receive are not affected by webhook auto-disable. + * Set webhook URL + */ + setWebhook(token: string, setWebhookRequest: SetWebhookRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + +} + +/** + * + */ +export class WebhooksApi extends runtime.BaseAPI implements WebhooksApiInterface { + + /** + * Creates request options for getWebhook without sending the request + */ + async getWebhookRequestOpts(requestParameters: GetWebhookRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling getWebhook().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/webhook`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Return the webhook URL(s) currently configured for this channel, plus delivery status for each URL. `webhookUrl` is a string when a single URL is set, an array of strings when multiple URLs are configured, or `null` if none is set. **Prefer `webhookStatuses`.** The circuit breaker is per URL, not per channel. If 1 of 4 URLs is sticky-disabled, that URL is skipped; the other three still receive events. Send/receive are not affected. Channel-level `haveDisabledWebhooks`, `circuitState`, `reason`, `fails`, `trips`, and timestamps are a compatibility summary of **one** URL (the first sticky-disabled URL, else the first configured URL). They are not \"the channel is dead\". - `haveDisabledWebhooks` — `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled. Live URLs still get events. - `circuitState` — compressed scalar for that same primary URL: `ok` | `degraded` | `open` | `half_open` | `disabled`. - `disabledWebhooks` — currently configured URLs that are sticky-disabled (subset of `webhookUrl`). - `webhookStatuses` — one row per currently configured URL. Use this array to see which endpoints are live vs skipped. - `fails` / `trips` / `firstFailAt` — **diagnostics for support**, not product UI. `fails` = failures in a rolling ~10 minute window (not lifetime). `trips` = failed recovery probes on the 15 min → 1 h → 6 h schedule (sticky disable after 3). Clients can ignore these and use `webhookStatuses` + `disabledWebhooks` only. Sticky disable skips delivery; it does not unconfigure the URL (it stays in `webhookUrl`). Re-enable with `POST /webhook` or `POST /settings`. Those endpoints **replace** the whole list — with multiple URLs, send the full array. + * Get webhook URL + */ + async getWebhookRaw(requestParameters: GetWebhookRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getWebhookRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => GetWebhook200ResponseFromJSON(jsonValue)); + } + + /** + * Return the webhook URL(s) currently configured for this channel, plus delivery status for each URL. `webhookUrl` is a string when a single URL is set, an array of strings when multiple URLs are configured, or `null` if none is set. **Prefer `webhookStatuses`.** The circuit breaker is per URL, not per channel. If 1 of 4 URLs is sticky-disabled, that URL is skipped; the other three still receive events. Send/receive are not affected. Channel-level `haveDisabledWebhooks`, `circuitState`, `reason`, `fails`, `trips`, and timestamps are a compatibility summary of **one** URL (the first sticky-disabled URL, else the first configured URL). They are not \"the channel is dead\". - `haveDisabledWebhooks` — `true` when **at least one** URL in `webhookUrl` is sticky auto-disabled. Live URLs still get events. - `circuitState` — compressed scalar for that same primary URL: `ok` | `degraded` | `open` | `half_open` | `disabled`. - `disabledWebhooks` — currently configured URLs that are sticky-disabled (subset of `webhookUrl`). - `webhookStatuses` — one row per currently configured URL. Use this array to see which endpoints are live vs skipped. - `fails` / `trips` / `firstFailAt` — **diagnostics for support**, not product UI. `fails` = failures in a rolling ~10 minute window (not lifetime). `trips` = failed recovery probes on the 15 min → 1 h → 6 h schedule (sticky disable after 3). Clients can ignore these and use `webhookStatuses` + `disabledWebhooks` only. Sticky disable skips delivery; it does not unconfigure the URL (it stays in `webhookUrl`). Re-enable with `POST /webhook` or `POST /settings`. Those endpoints **replace** the whole list — with multiple URLs, send the full array. + * Get webhook URL + */ + async getWebhook(token: string, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getWebhookRaw({ token: token }, initOverrides); + return await response.value(); + } + + /** + * Creates request options for setWebhook without sending the request + */ + async setWebhookRequestOpts(requestParameters: SetWebhookOperationRequest): Promise { + if (requestParameters['token'] == null) { + throw new runtime.RequiredError( + 'token', + 'Required parameter "token" was null or undefined when calling setWebhook().' + ); + } + + if (requestParameters['setWebhookRequest'] == null) { + throw new runtime.RequiredError( + 'setWebhookRequest', + 'Required parameter "setWebhookRequest" was null or undefined when calling setWebhook().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['token'] != null) { + queryParameters['token'] = requestParameters['token']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + queryParameters["token"] = await this.configuration.apiKey("token"); // tokenAuth authentication + } + + + let urlPath = `/webhook`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: SetWebhookRequestToJSON(requestParameters['setWebhookRequest']), + }; + } + + /** + * Set the client webhook URL that receives inbound events for this channel. **Replaces** the stored webhook list (it does not append). To deliver the same events to several endpoints, pass an array in `webhookUrl` (max 5) or use `POST /settings`. With several URLs already configured, posting only the disabled URL **drops the others** — send the complete array to re-enable one of many. Incoming payloads include messages, delivery ACKs, group updates, coexistence echoes, and calling events. WhatsApp Calling payloads (`type: \"calls\"`) are documented under the **Calling** tag. Call permission replies arrive on the normal messages path (`call_permission_reply`). Repeating `POST /webhook` with the same URL(s) clears an auto-disable (self-serve re-enable). Send/receive are not affected by webhook auto-disable. + * Set webhook URL + */ + async setWebhookRaw(requestParameters: SetWebhookOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.setWebhookRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SetWebhook200ResponseFromJSON(jsonValue)); + } + + /** + * Set the client webhook URL that receives inbound events for this channel. **Replaces** the stored webhook list (it does not append). To deliver the same events to several endpoints, pass an array in `webhookUrl` (max 5) or use `POST /settings`. With several URLs already configured, posting only the disabled URL **drops the others** — send the complete array to re-enable one of many. Incoming payloads include messages, delivery ACKs, group updates, coexistence echoes, and calling events. WhatsApp Calling payloads (`type: \"calls\"`) are documented under the **Calling** tag. Call permission replies arrive on the normal messages path (`call_permission_reply`). Repeating `POST /webhook` with the same URL(s) clears an auto-disable (self-serve re-enable). Send/receive are not affected by webhook auto-disable. + * Set webhook URL + */ + async setWebhook(token: string, setWebhookRequest: SetWebhookRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.setWebhookRaw({ token: token, setWebhookRequest: setWebhookRequest }, initOverrides); + return await response.value(); + } + +} diff --git a/quality/mcp/vendor/generate-mcp-tools.cjs b/quality/mcp/vendor/generate-mcp-tools.cjs new file mode 100644 index 0000000..34e0fb2 --- /dev/null +++ b/quality/mcp/vendor/generate-mcp-tools.cjs @@ -0,0 +1,900 @@ +#!/usr/bin/env node +/** + * Generate MCP tool definitions and SDK handler map from bundled public OpenAPI. + * + * ARCHITECTURE: handlers MUST delegate to @1msg/sdk (Client) only. + * Do not emit fetch/axios or raw HTTP — see packages/mcp/ARCHITECTURE.md + * + * Input: public/openapi.bundled.yaml + * Output: packages/mcp/src/tools.generated.ts + * packages/mcp/src/handlers.generated.ts + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('yaml'); + +const ROOT = path.resolve(__dirname, '..'); +const INPUT_SPEC = path.join(ROOT, 'public/openapi.bundled.yaml'); +const OUTPUT_DIR = path.join(ROOT, 'packages/mcp/src'); +const TOOLS_FILE = path.join(OUTPUT_DIR, 'tools.generated.ts'); +const HANDLERS_FILE = path.join(OUTPUT_DIR, 'handlers.generated.ts'); + +const TAG_TO_API = { + Profile: 'profile', + Messaging: 'messaging', + Users: 'users', + Groups: 'groups', + Flows: 'flows', + Templates: 'templates', + Channel: 'channel', + Catalog: 'catalog', + Webhooks: 'webhooks', + Calling: 'calling', +}; + +const BODY_OBJECT_METHODS = new Set([ + 'sendButton', + 'sendContact', + 'sendList', + 'sendLocationRequest', + 'sendReaction', + 'sendProduct', + 'sendTemplate', + 'sendCtaUrl', + 'sendSticker', + 'sendAddressMessage', + 'sendOrderDetails', + 'sendPaymentRequest', + 'createUploadMedia', + 'setWhatsappBusinessEncryption', + 'deleteMediaLegacy', + 'blockUser', + 'unblockUser', + 'addTemplate', + 'removeTemplate', + 'updateCallingSettings', + 'initiateCall', + 'setConversationalAutomation', + 'setWebhook', + 'updateMe', + 'createCommerce', + 'createSettings', + 'createGroups', + 'createGroupsGroupId', + 'createFlows', + 'patchFlowsFlowIdAssets', + 'patchFlowsFlowIdMetadata', +]); + +/** OpenAPI boolean query/body fields that SDK types as boolean (optionalArg returns string). */ +const BOOLEAN_PARAMS = new Set(['typingIndicator', 'voice', 'last', 'publish']); + +/** OpenAPI enum fields that need a cast through `as never` for SDK enums. */ +const ENUM_PARAMS = new Set(['mediaType', 'sort']); + +function camelToSnake(value) { + return value + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + .toLowerCase(); +} + +function loadSpec() { + if (!fs.existsSync(INPUT_SPEC)) { + console.error(`❌ Bundled OpenAPI spec not found: ${INPUT_SPEC}`); + console.error( + ' Run `npm run bundle:public` from the repo root, then retry.', + ); + process.exit(1); + } + return yaml.parse(fs.readFileSync(INPUT_SPEC, 'utf8')); +} + +function resolveRef(spec, ref) { + if (!ref || !ref.startsWith('#/')) { + return ref; + } + const parts = ref.slice(2).split('/'); + let node = spec; + for (const part of parts) { + node = node?.[part]; + } + return node; +} + +function resolveSchema(spec, schema) { + if (!schema) { + return { type: 'object', properties: {}, additionalProperties: true }; + } + if (schema.$ref) { + return resolveSchema(spec, resolveRef(spec, schema.$ref)); + } + if (schema.allOf) { + const merged = { type: 'object', properties: {}, required: [] }; + for (const part of schema.allOf) { + const resolved = resolveSchema(spec, part); + merged.properties = { + ...merged.properties, + ...(resolved.properties || {}), + }; + merged.required = [ + ...new Set([...(merged.required || []), ...(resolved.required || [])]), + ]; + if (resolved.description) { + merged.description = resolved.description; + } + } + return merged; + } + return schema; +} + +function mergeParameters(spec, pathItemParams, operationParams) { + const byKey = new Map(); + for (const param of [...(pathItemParams || []), ...(operationParams || [])]) { + const resolved = param.$ref ? resolveRef(spec, param.$ref) : param; + if (!resolved || resolved.name === 'token') { + continue; + } + byKey.set(`${resolved.in}:${resolved.name}`, resolved); + } + return [...byKey.values()]; +} + +function pickRequestBodySchema(spec, requestBody) { + if (!requestBody) { + return null; + } + const content = requestBody.content || {}; + const preferredOrder = [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + ...Object.keys(content), + ]; + const seen = new Set(); + const schemas = []; + for (const key of preferredOrder) { + if (seen.has(key) || !content[key]?.schema) { + continue; + } + seen.add(key); + schemas.push(resolveSchema(spec, content[key].schema)); + } + if (schemas.length === 0) { + return null; + } + return schemas.reduce((acc, schema) => mergeResolvedSchemas(acc, schema)); +} + +function isEmptySchemaNode(node) { + return !node || (typeof node === 'object' && Object.keys(node).length === 0); +} + +function mergePropSchema(preferred, fallback) { + if (!preferred) { + return fallback; + } + if (!fallback) { + return preferred; + } + const merged = { ...fallback, ...preferred }; + if (!preferred.description && fallback.description) { + merged.description = fallback.description; + } + if (isEmptySchemaNode(preferred.items) && fallback.items) { + merged.items = fallback.items; + } + if (!preferred.example && fallback.example) { + merged.example = fallback.example; + } + if ( + (!preferred.properties || Object.keys(preferred.properties).length === 0) && + fallback.properties + ) { + merged.properties = fallback.properties; + } + return merged; +} + +function mergeResolvedSchemas(preferred, fallback) { + if (!preferred) { + return fallback; + } + if (!fallback) { + return preferred; + } + const names = new Set([ + ...Object.keys(preferred.properties || {}), + ...Object.keys(fallback.properties || {}), + ]); + const properties = {}; + for (const name of names) { + properties[name] = mergePropSchema( + preferred.properties?.[name], + fallback.properties?.[name], + ); + } + return { + ...fallback, + ...preferred, + properties, + required: [ + ...new Set([...(preferred.required || []), ...(fallback.required || [])]), + ], + description: preferred.description || fallback.description, + }; +} + +function buildInputSchema(spec, parameters, bodySchema) { + const properties = {}; + const required = []; + + for (const param of parameters) { + const schema = resolveSchema(spec, param.schema || { type: 'string' }); + properties[param.name] = { + ...schema, + description: param.description || schema.description, + }; + if (param.required) { + required.push(param.name); + } + } + + if (bodySchema) { + const resolvedBody = resolveSchema(spec, bodySchema); + const bodyProps = resolvedBody.properties || {}; + for (const [name, propSchema] of Object.entries(bodyProps)) { + if (properties[name]) { + continue; + } + properties[name] = resolveSchema(spec, propSchema); + } + for (const name of resolvedBody.required || []) { + if (!required.includes(name)) { + required.push(name); + } + } + if ( + resolvedBody.additionalProperties === true && + Object.keys(bodyProps).length === 0 + ) { + properties.payload = { + type: 'object', + additionalProperties: true, + description: 'Request body payload', + }; + } + } + + const schema = { + type: 'object', + properties, + additionalProperties: false, + }; + if (required.length > 0) { + schema.required = required; + } + return schema; +} + +function collectOperations(spec) { + const operations = []; + for (const [routePath, pathItem] of Object.entries(spec.paths || {})) { + const pathLevelParams = pathItem.parameters || []; + for (const method of ['get', 'post', 'put', 'patch', 'delete']) { + const operation = pathItem[method]; + if (!operation?.operationId) { + continue; + } + const tag = operation.tags?.[0] || 'Messaging'; + const parameters = mergeParameters( + spec, + pathLevelParams, + operation.parameters, + ); + const bodySchema = pickRequestBodySchema(spec, operation.requestBody); + const inputSchema = buildInputSchema(spec, parameters, bodySchema); + // Preserve the existing MCP numeric-ID convenience; the handler serializes it + // through requireArg before the SDK sees the wire-level string identifier. + if ( + operation.operationId === 'deleteMedia' && + inputSchema.properties.mediaId + ) { + inputSchema.properties.mediaId.type = ['string', 'number']; + } + operations.push({ + operationId: operation.operationId, + toolName: camelToSnake(operation.operationId), + summary: operation.summary || operation.operationId, + description: + operation.description || operation.summary || operation.operationId, + tag, + apiClass: TAG_TO_API[tag] || 'messaging', + parameters, + bodySchema, + inputSchema, + }); + } + } + operations.sort((a, b) => a.operationId.localeCompare(b.operationId)); + return operations; +} + +function resolveSdkApisDir() { + const srcDir = path.join(ROOT, 'packages/sdk/src/generated/src/apis'); + if (fs.existsSync(srcDir)) { + return srcDir; + } + const distDir = path.join(ROOT, 'packages/sdk/dist/generated/src/apis'); + if (fs.existsSync(distDir)) { + return distDir; + } + return srcDir; +} + +function parseSdkMethodSignatures() { + const signatures = new Map(); + const sdkApisDir = resolveSdkApisDir(); + if (!fs.existsSync(sdkApisDir)) { + console.warn(`⚠️ SDK APIs directory not found: ${sdkApisDir}`); + return signatures; + } + const apiFiles = fs + .readdirSync(sdkApisDir) + .filter((f) => f.endsWith('Api.ts') || f.endsWith('Api.d.ts')); + + for (const file of apiFiles) { + const apiClass = file.replace(/Api(?:\.d)?\.ts$/, ''); + const apiKey = apiClass.charAt(0).toLowerCase() + apiClass.slice(1); + const content = fs.readFileSync(path.join(sdkApisDir, file), 'utf8'); + const ifaceMatch = content.match( + /export interface \w+ApiInterface \{([\s\S]*?)\n\}/, + ); + if (!ifaceMatch) { + continue; + } + const ifaceBody = ifaceMatch[1]; + const methodRegex = /^\s+(\w+)\(([^)]*)\):/gm; + let match; + while ((match = methodRegex.exec(ifaceBody)) !== null) { + const methodName = match[1]; + if (methodName.endsWith('Raw') || methodName.endsWith('RequestOpts')) { + continue; + } + const params = match[2] + .split(',') + .map((p) => p.trim()) + .filter(Boolean) + .map((p) => { + const optional = p.includes('?'); + const name = p.split(':')[0].replace('?', '').trim(); + return { name, optional }; + }) + .filter((p) => p.name !== 'initOverrides'); + signatures.set(methodName, { apiClass: apiKey, params }); + } + } + return signatures; +} + +function camelToSnakeCandidates(name) { + return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} + +/** SDK camelCases query names (`minTime`); MCP schema keeps OpenAPI names (`min_time`). */ +function openApiArgName(sdkParamName, operation) { + const params = operation.parameters || []; + if (params.some((param) => param.name === sdkParamName)) { + return sdkParamName; + } + const snake = camelToSnakeCandidates(sdkParamName); + if (params.some((param) => param.name === snake)) { + return snake; + } + return sdkParamName; +} + +function openApiParamType(sdkParamName, operation) { + const openApiName = openApiArgName(sdkParamName, operation); + const match = (operation.parameters || []).find( + (param) => param.name === openApiName || param.name === sdkParamName, + ); + return match?.schema?.type; +} + +function argAccessor(paramName, optional, operation) { + const argsKey = operation ? openApiArgName(paramName, operation) : paramName; + const schemaType = operation + ? openApiParamType(paramName, operation) + : undefined; + if ( + paramName === 'phone' || + schemaType === 'integer' || + schemaType === 'number' + ) { + return optional + ? `optionalNumber(args, '${argsKey}')` + : `requireNumber(args, '${argsKey}')`; + } + if (paramName === 'params') { + return optional + ? `optionalArray(args, '${argsKey}')` + : `requireArray(args, '${argsKey}')`; + } + if ( + BOOLEAN_PARAMS.has(paramName) || + BOOLEAN_PARAMS.has(argsKey) || + schemaType === 'boolean' + ) { + return optional + ? `optionalBoolean(args, '${argsKey}')` + : `requireBoolean(args, '${argsKey}')`; + } + if (ENUM_PARAMS.has(paramName)) { + return optional + ? `optionalArg(args, '${argsKey}') as never` + : `requireArg(args, '${argsKey}') as never`; + } + return optional + ? `optionalArg(args, '${argsKey}')` + : `requireArg(args, '${argsKey}')`; +} + +function pathOrQueryParamNames(operation) { + return (operation.parameters || []) + .filter((param) => param.in === 'path' || param.in === 'query') + .map((param) => param.name); +} + +function isPathOrQuerySdkParam(sdkName, operation) { + const names = pathOrQueryParamNames(operation); + if (names.includes(sdkName)) { + return true; + } + return names.includes(camelToSnakeCandidates(sdkName)); +} + +function emitBodyObjectHandler(operation, signature) { + const { operationId, apiClass } = operation; + const sdkParams = (signature?.params || []).filter( + (param) => param.name !== 'initOverrides', + ); + const bodySdkParam = sdkParams.find( + (param) => + param.name !== 'token' && !isPathOrQuerySdkParam(param.name, operation), + ); + const prelude = ['const token = client.config.token;']; + const pathAndQueryNames = pathOrQueryParamNames(operation); + + if (operation.inputSchema.properties?.payload) { + prelude.push( + 'const bodyArg = (args.payload ?? args) as Record;', + ); + } else { + prelude.push('const bodyArg = pickArgs(args, inputFields);'); + for (const name of pathAndQueryNames) { + prelude.push(`delete bodyArg['${name}'];`); + } + } + + if (operationId === 'sendContact') { + prelude.push(`if (bodyArg['contact'] && !bodyArg['contacts']) { + const c = bodyArg['contact'] as Record; + const name = typeof c.name === 'string' + ? { formatted_name: c.name, first_name: String(c.name).split(/\\s+/)[0] } + : c.name; + bodyArg['contacts'] = [{ + name, + phones: c.phone ? [{ phone: String(c.phone), type: 'CELL' }] : undefined, + emails: c.email ? [{ email: c.email }] : undefined, + org: c.org, + urls: c.url ? [{ url: c.url }] : undefined, + }]; + delete bodyArg['contact']; + }`); + } + + if (operationId === 'createUploadMedia') { + prelude.push(`if (typeof bodyArg['url'] === 'string' && (bodyArg['body'] === undefined || bodyArg['body'] === '')) { + bodyArg['body'] = bodyArg['url']; + }`); + } + + const callArgs = []; + if (sdkParams.length === 0) { + return { + prelude, + call: `return client.${apiClass}.${operationId}(token, bodyArg as never);`, + }; + } + for (const param of sdkParams) { + if (param.name === 'token') { + callArgs.push('token'); + continue; + } + if (bodySdkParam && param.name === bodySdkParam.name) { + callArgs.push('bodyArg as never'); + continue; + } + prelude.push( + `const ${param.name} = ${argAccessor(param.name, param.optional, operation)};`, + ); + callArgs.push(param.name); + } + + return { + prelude, + call: `return client.${apiClass}.${operationId}(${callArgs.join(', ')});`, + }; +} + +function fixHandlerCallOrder(operation, signature) { + const { operationId, apiClass } = operation; + const params = + signature?.params?.filter((p) => p.name !== 'initOverrides') || []; + const nonToken = params.filter((p) => p.name !== 'token'); + + if (BODY_OBJECT_METHODS.has(operationId)) { + return emitBodyObjectHandler(operation, signature); + } + + if (nonToken.length === 0) { + return { + prelude: ['const token = client.config.token;'], + call: `return client.${apiClass}.${operationId}(token);`, + }; + } + + if (operationId === 'deleteMedia') { + return { + prelude: [ + 'const token = client.config.token;', + "const mediaId = requireArg(args, 'mediaId');", + ], + call: 'return client.messaging.deleteMedia(token, mediaId);', + }; + } + + if (operationId === 'sendCarousel') { + const carouselPrelude = ['const token = client.config.token;']; + carouselPrelude.push( + "const cards = optionalArray(args, 'cards') as object[] | undefined;", + ); + carouselPrelude.push( + "let params = optionalArray(args, 'params') as object[] | undefined;", + ); + carouselPrelude.push( + "if ((!params || params.length === 0) && cards?.length) { params = [{ type: 'CAROUSEL', cards }]; }", + ); + carouselPrelude.push( + "if (!params?.length) throw new Error('send_carousel requires cards (2-10) or params with a CAROUSEL component');", + ); + carouselPrelude.push("const body = optionalArg(args, 'body');"); + carouselPrelude.push( + "const quotedMsgId = optionalArg(args, 'quotedMsgId');", + ); + carouselPrelude.push("const chatId = optionalArg(args, 'chatId');"); + carouselPrelude.push("const phone = optionalNumber(args, 'phone');"); + return { + prelude: carouselPrelude, + call: 'return client.messaging.sendCarousel(token, params, body, quotedMsgId, chatId, phone);', + }; + } + + if (operationId === 'sendFlow') { + const flowPrelude = ['const token = client.config.token;']; + flowPrelude.push("const body = requireArg(args, 'body');"); + flowPrelude.push("const flowId = requireArg(args, 'flowId');"); + flowPrelude.push("const flowToken = requireArg(args, 'flowToken');"); + flowPrelude.push("const flowCta = requireArg(args, 'flowCta');"); + flowPrelude.push( + 'const header = args.header as Record | undefined;', + ); + flowPrelude.push("const footer = optionalArg(args, 'footer');"); + flowPrelude.push( + 'const flowAction = args.flowAction as string | undefined;', + ); + flowPrelude.push( + 'const flowActionPayload = args.flowActionPayload as object | undefined;', + ); + flowPrelude.push( + "const flowMessageVersion = optionalArg(args, 'flowMessageVersion');", + ); + flowPrelude.push('const mode = args.mode as string | undefined;'); + flowPrelude.push( + 'const flowActionData = args.flowActionData as object | undefined;', + ); + flowPrelude.push( + "const flowActionScreen = optionalArg(args, 'flowActionScreen');", + ); + flowPrelude.push("const quotedMsgId = optionalArg(args, 'quotedMsgId');"); + flowPrelude.push("const chatId = optionalArg(args, 'chatId');"); + flowPrelude.push("const phone = optionalNumber(args, 'phone');"); + return { + prelude: flowPrelude, + call: 'return client.messaging.sendFlow(token, body, flowId, flowToken, flowCta, header as never, footer, flowAction as never, flowActionPayload, flowMessageVersion, mode as never, flowActionData, flowActionScreen, quotedMsgId, chatId, phone);', + }; + } + + const pathParamNames = new Set( + operation.parameters.filter((p) => p.in === 'path').map((p) => p.name), + ); + const pathFirst = nonToken.some((p) => pathParamNames.has(p.name)); + + const prelude = ['const token = client.config.token;']; + + if (pathFirst) { + for (const param of nonToken) { + prelude.push( + `const ${param.name} = ${argAccessor(param.name, param.optional, operation)};`, + ); + } + const ordered = []; + for (const param of nonToken) { + if (pathParamNames.has(param.name)) { + ordered.push(param.name); + } + } + ordered.push('token'); + for (const param of nonToken) { + if (!pathParamNames.has(param.name)) { + ordered.push(param.name); + } + } + return { + prelude, + call: `return client.${apiClass}.${operationId}(${ordered.join(', ')});`, + }; + } + + for (const param of nonToken) { + prelude.push( + `const ${param.name} = ${argAccessor(param.name, param.optional, operation)};`, + ); + } + + if (operationId === 'sendMessage') { + return { + prelude, + call: 'return client.sendMessage({ body, quotedMsgId, chatId, phone });', + }; + } + + return { + prelude, + call: `return client.${apiClass}.${operationId}(token, ${nonToken.map((p) => p.name).join(', ')});`, + }; +} + +function emitToolsFile(operations) { + const body = `/* eslint-disable */ +/* auto-generated by gen/generate-mcp-tools.js — do not edit */ + +export interface JsonSchema { + type?: string | string[]; + properties?: Record; + required?: string[]; + items?: JsonSchema; + description?: string; + additionalProperties?: boolean | JsonSchema; + enum?: unknown[]; + oneOf?: JsonSchema[]; + format?: string; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + [key: string]: unknown; +} + +export interface GeneratedMcpTool { + /** MCP tool name (snake_case) */ + name: string; + /** OpenAPI operationId */ + operationId: string; + description: string; + inputSchema: JsonSchema; + inputFields: string[]; +} + +export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = ${JSON.stringify( + operations.map((op) => ({ + name: op.toolName, + operationId: op.operationId, + description: op.description, + inputSchema: op.inputSchema, + inputFields: Object.keys(op.inputSchema.properties || {}), + })), + null, + 2, + )}; + +export const GENERATED_MCP_TOOL_COUNT = GENERATED_MCP_TOOLS.length; +`; + + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + fs.writeFileSync(TOOLS_FILE, body); +} + +function emitHandlersFile(operations, signatures) { + const cases = operations.map((operation) => { + const signature = signatures.get(operation.operationId); + const apiClass = signature?.apiClass || operation.apiClass; + const handler = fixHandlerCallOrder({ ...operation, apiClass }, signature); + const body = [...handler.prelude, handler.call] + .map((line) => ` ${line}`) + .join('\n'); + return ` case '${operation.toolName}': {\n${body}\n }`; + }); + + const body = `/* eslint-disable */ +/* auto-generated by gen/generate-mcp-tools.js — do not edit */ +// @ts-nocheck — generated against workspace SDK; published @1msg/sdk may lag (settings polyfill). + +import type { Client } from '@1msg/sdk'; +import { GENERATED_MCP_TOOLS } from './tools.generated'; + +function requireArg(args: Record, key: string): string { + const value = args[key]; + if (value === undefined || value === null || value === '') { + throw new Error(\`Missing required argument: \${key}\`); + } + return String(value); +} + +function optionalArg(args: Record, key: string): string | undefined { + const value = args[key]; + if (value === undefined || value === null || value === '') { + return undefined; + } + return typeof value === 'string' ? value : String(value); +} + +function optionalNumber(args: Record, key: string): number | undefined { + const value = args[key]; + if (value === undefined || value === null || value === '') { + return undefined; + } + if (typeof value === 'number') { + return value; + } + const parsed = Number(value); + return Number.isNaN(parsed) ? undefined : parsed; +} + +function requireNumber(args: Record, key: string): number { + const value = optionalNumber(args, key); + if (value === undefined) { + throw new Error(\`Missing required argument: \${key}\`); + } + return value; +} + +function optionalBoolean(args: Record, key: string): boolean | undefined { + const value = args[key]; + if (value === undefined || value === null || value === '') { + return undefined; + } + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'number') { + return value !== 0; + } + const normalized = String(value).trim().toLowerCase(); + if (normalized === 'true' || normalized === '1') { + return true; + } + if (normalized === 'false' || normalized === '0') { + return false; + } + return undefined; +} + +function requireBoolean(args: Record, key: string): boolean { + const value = optionalBoolean(args, key); + if (value === undefined) { + throw new Error(\`Missing required argument: \${key}\`); + } + return value; +} + +function optionalArray(args: Record, key: string): unknown[] | undefined { + const value = args[key]; + if (value === undefined || value === null) { + return undefined; + } + if (Array.isArray(value)) { + return value; + } + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : undefined; + } catch { + return undefined; + } + } + return undefined; +} + +function requireArray(args: Record, key: string): unknown[] { + const value = optionalArray(args, key); + if (!value) { + throw new Error(\`Missing required argument: \${key} (expected array)\`); + } + return value; +} + +function pickArgs(args: Record, keys: string[]): Record { + const picked: Record = {}; + for (const key of keys) { + if (args[key] !== undefined && args[key] !== null && args[key] !== '') { + picked[key] = args[key]; + } + } + return picked; +} + +const inputFieldsByTool = new Map( + GENERATED_MCP_TOOLS.map((tool) => [tool.name, tool.inputFields]), +); + +export async function invokeGeneratedTool( + client: Client, + toolName: string, + args: Record, +): Promise { + const inputFields = inputFieldsByTool.get(toolName) ?? Object.keys(args); + + switch (toolName) { +${cases.join('\n\n')} + default: + throw new Error(\`Unknown tool: \${toolName}\`); + } +} +`; + + fs.writeFileSync(HANDLERS_FILE, body); +} + +function main() { + console.log('🔧 Chat API MCP Tools Generator'); + console.log('─'.repeat(50)); + + const spec = loadSpec(); + const operations = collectOperations(spec); + const signatures = parseSdkMethodSignatures(); + + if (operations.length === 0) { + console.error('❌ No operations with operationId found in bundled spec.'); + process.exit(1); + } + + const missing = operations.filter((op) => !signatures.has(op.operationId)); + if (missing.length > 0) { + console.warn( + `⚠️ ${missing.length} operations missing SDK signature (using tag fallback):`, + ); + for (const op of missing) { + console.warn(` - ${op.operationId}`); + } + } + + emitToolsFile(operations); + emitHandlersFile(operations, signatures); + + console.log(`📝 Input: ${path.relative(ROOT, INPUT_SPEC)}`); + console.log(`📦 Output: ${path.relative(ROOT, TOOLS_FILE)}`); + console.log(`📦 Output: ${path.relative(ROOT, HANDLERS_FILE)}`); + console.log(`📊 Generated ${operations.length} MCP tools`); + console.log(''); + console.log('✅ MCP tools generated successfully'); + console.log('─'.repeat(50)); +} + +if (require.main === module) main(); +module.exports = { collectOperations }; diff --git a/quality/mcp/verify-negative.mjs b/quality/mcp/verify-negative.mjs new file mode 100644 index 0000000..fc36e2a --- /dev/null +++ b/quality/mcp/verify-negative.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { readFileSync, rmSync, writeFileSync } from 'node:fs'; +const file = 'src/tools.generated.ts'; +const unexpected = 'src/quality-fixture.generated.ts'; +const original = readFileSync(file); +const ignore = readFileSync('.gitignore'); +/** Execute the real producer and require specifically a drift failure, never an unrelated tool or dependency failure. */ +function rejectDrift() { + const result = spawnSync(process.execPath, ['quality/mcp/generate.mjs'], { + encoding: 'utf8', + timeout: 120000, + }); + assert.notEqual(result.status, 0, 'MCP regeneration accepted modified output'); + assert.match(result.stderr + result.stdout, /Generated drift/); +} +try { + writeFileSync(file, Buffer.concat([original, Buffer.from('\n// drift fixture\n')])); + rejectDrift(); + rmSync(file); + rejectDrift(); + writeFileSync(file, original); + writeFileSync('.gitignore', Buffer.concat([ignore, Buffer.from(`\n/${unexpected}\n`)])); + writeFileSync(unexpected, 'export const unexpected = true;\n'); + rejectDrift(); + console.log('Real MCP generation rejects changed, deleted and ignored/untracked output'); +} finally { + writeFileSync(file, original); + writeFileSync('.gitignore', ignore); + rmSync(unexpected, { force: true }); +} diff --git a/scripts/pack-smoke.mjs b/scripts/pack-smoke.mjs new file mode 100644 index 0000000..dcd8c2b --- /dev/null +++ b/scripts/pack-smoke.mjs @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +const root = process.cwd(); +const temporary = mkdtempSync(join(tmpdir(), '1msg-packed-')); +/** Run the artifact consumer and propagate both command failure and unexpected hangs. */ +function run(command, args, cwd = root) { + const result = spawnSync(command, args, { cwd, encoding: 'utf8', timeout: 30000 }); + if (result.status !== 0) + throw new Error(`${command} failed: ${result.stderr || result.stdout || result.error}`); + return result.stdout; +} +try { + const [packed] = JSON.parse( + run('npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', temporary]), + ); + assert.ok( + packed.files.some((file) => file.path.startsWith('dist/')), + 'packed build output missing', + ); + run('tar', ['-xzf', join(temporary, packed.filename), '-C', temporary]); + const artifact = join(temporary, 'package'); + // The artifact is isolated from source; dependencies come from the frozen checkout install. + symlinkSync(resolve('node_modules'), join(artifact, 'node_modules'), 'dir'); + const manifest = JSON.parse(readFileSync(join(artifact, 'package.json'), 'utf8')); + if (manifest.n8n) { + for (const entry of [...manifest.n8n.nodes, ...manifest.n8n.credentials]) { + run( + process.execPath, + [ + '-e', + `const value=require(${JSON.stringify(join(artifact, entry))}); if(!Object.values(value).some(item=>typeof item==='function')) throw new Error('Missing registered node/credential export')`, + ], + artifact, + ); + } + } else if (manifest.bin) { + const entry = typeof manifest.bin === 'string' ? manifest.bin : Object.values(manifest.bin)[0]; + assert.match(run(process.execPath, [join(artifact, entry), '--help'], artifact), /1msg/i); + } else { + run( + process.execPath, + ['-e', `require(${JSON.stringify(join(artifact, manifest.main))})`], + artifact, + ); + } + console.log( + 'Packed artifact resolves and its public entrypoints load without source files or external services', + ); +} finally { + rmSync(temporary, { recursive: true, force: true }); +} diff --git a/scripts/quality-negative.mjs b/scripts/quality-negative.mjs new file mode 100644 index 0000000..e9392eb --- /dev/null +++ b/scripts/quality-negative.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; + +const root = process.argv[2] ?? 'src'; +const directory = join(root, '__quality_negative_fixtures__'); +if (existsSync(directory)) throw new Error(`Refusing to overwrite ${directory}`); +mkdirSync(directory); + +/** Run the repository's actual analyzer and require the intended diagnostic, not an unrelated setup failure. */ +function rejects(command, args, expected) { + const result = spawnSync(process.execPath, [command, ...args], { + encoding: 'utf8', + env: { ...process.env, CI: 'true', ESLINT_SKIP_TYPE_AWARE: 'true' }, + }); + const output = result.stdout + result.stderr; + assert.notEqual(result.status, 0, 'negative fixture unexpectedly passed'); + assert.match(output, expected, `wrong failure: ${output}`); +} + +try { + const fixture = join(directory, 'unsafe.ts'); + writeFileSync( + fixture, + '/** Negative fixture: checks that typed lint cannot be disabled by environment flags. */\nexport function unsafe(value: any) { Promise.resolve(1); return value.field; }\n', + ); + rejects( + 'tools/quality/node_modules/eslint/bin/eslint.js', + ['-c', 'eslint.typed.config.mjs', fixture], + /no-floating-promises/, + ); + rejects( + 'tools/quality/node_modules/eslint/bin/eslint.js', + ['-c', 'eslint.typed.config.mjs', fixture], + /no-unsafe-member-access/, + ); + for (const [name, next] of [ + ['a', 'b'], + ['b', 'c'], + ['c', 'a'], + ]) { + writeFileSync( + join(directory, `${name}.ts`), + `import { ${next} } from './${next}.js';\nexport const ${name} = () => ${next}();\n`, + ); + } + rejects( + 'tools/quality/node_modules/dependency-cruiser/bin/dependency-cruiser.mjs', + ['--config', '.dependency-cruiser.cjs', directory], + /no-runtime-cycles/, + ); + console.log( + 'Negative fixtures reject floating promises, unsafe access, skip flags and a three-edge runtime cycle', + ); +} finally { + rmSync(directory, { recursive: true, force: true }); +} diff --git a/scripts/smoke-tools-list.js b/scripts/smoke-tools-list.js index cbd7fca..7009f80 100644 --- a/scripts/smoke-tools-list.js +++ b/scripts/smoke-tools-list.js @@ -1,17 +1,19 @@ #!/usr/bin/env node +const { Client } = require('@modelcontextprotocol/sdk/client'); +const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); /** * Smoke test: connect to MCP server via stdio and verify tools/list. */ -const path = require('path'); -const { Client } = require('@modelcontextprotocol/sdk/client'); -const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); +const path = require('node:path'); async function main() { const serverEntry = path.resolve(__dirname, '../dist/index.js'); const env = { ...process.env, - ONE_MSG_BASE_URL: process.env.ONE_MSG_BASE_URL || process.env.CHAT_API_BASE_URL || 'https://api.1msg.io', - ONE_MSG_INSTANCE_ID: process.env.ONE_MSG_INSTANCE_ID || process.env.CHAT_API_INSTANCE_ID || 'TEST_INSTANCE', + ONE_MSG_BASE_URL: + process.env.ONE_MSG_BASE_URL || process.env.CHAT_API_BASE_URL || 'https://api.1msg.io', + ONE_MSG_INSTANCE_ID: + process.env.ONE_MSG_INSTANCE_ID || process.env.CHAT_API_INSTANCE_ID || 'TEST_INSTANCE', ONE_MSG_TOKEN: process.env.ONE_MSG_TOKEN || process.env.CHAT_API_TOKEN || 'test-token', }; diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts index 172aead..6539ea8 100644 --- a/src/__tests__/auth.test.ts +++ b/src/__tests__/auth.test.ts @@ -1,10 +1,6 @@ import type { IncomingMessage } from 'node:http'; -import { - extractBearerToken, - extractInstanceId, - McpAuthError, - resolveRequestConfig, -} from '../auth'; + +import { extractBearerToken, extractInstanceId, McpAuthError, resolveRequestConfig } from '../auth'; function headers(map: Record): IncomingMessage['headers'] { const out: IncomingMessage['headers'] = {}; @@ -26,15 +22,11 @@ describe('MCP HTTP auth', () => { }); it('accepts X-1msg-Instance-Id alias', () => { - expect(extractInstanceId(headers({ 'x-1msg-instance-id': 'ODI2' }))).toBe( - 'ODI2', - ); + expect(extractInstanceId(headers({ 'x-1msg-instance-id': 'ODI2' }))).toBe('ODI2'); }); it('accepts deprecated X-Chat-Api-Instance-Id alias', () => { - expect( - extractInstanceId(headers({ 'x-chat-api-instance-id': 'ODI3' })), - ).toBe('ODI3'); + expect(extractInstanceId(headers({ 'x-chat-api-instance-id': 'ODI3' }))).toBe('ODI3'); }); it('requires instance id header', () => { diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 7c2606b..9310bb8 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -5,15 +5,11 @@ describe('normalizeBaseUrl', () => { expect(normalizeBaseUrl('https://api.1msg.io')).toBe('https://api.1msg.io'); expect(normalizeBaseUrl('https://api.1msg.io/')).toBe('https://api.1msg.io'); expect(normalizeBaseUrl('sandbox.1msg.io')).toBe('https://sandbox.1msg.io'); - expect(normalizeBaseUrl('http://sandbox.1msg.io')).toBe( - 'https://sandbox.1msg.io', - ); + expect(normalizeBaseUrl('http://sandbox.1msg.io')).toBe('https://sandbox.1msg.io'); expect(normalizeBaseUrl('https://sandbox.1msg.io/HEI123/status')).toBe( 'https://sandbox.1msg.io', ); - expect(normalizeBaseUrl('https://api.sandbox.1msg.io')).toBe( - 'https://sandbox.1msg.io', - ); + expect(normalizeBaseUrl('https://api.sandbox.1msg.io')).toBe('https://sandbox.1msg.io'); }); it('keeps custom hosts (origin only)', () => { diff --git a/src/__tests__/handlers.test.ts b/src/__tests__/handlers.test.ts index 66a58dc..29deb85 100644 --- a/src/__tests__/handlers.test.ts +++ b/src/__tests__/handlers.test.ts @@ -1,4 +1,5 @@ import type { Client } from '@1msg/sdk'; + import { invokeGeneratedTool } from '../handlers.generated'; import { GENERATED_MCP_TOOLS } from '../tools.generated'; @@ -26,6 +27,7 @@ function createMockClient(): Client { } as unknown as Client; } +/** Verify generated dispatch and compatibility normalization against a fake SDK without contacting upstream services. */ describe('invokeGeneratedTool', () => { it('delegates send_message to ChatApiClient.sendMessage', async () => { const client = createMockClient(); @@ -64,6 +66,7 @@ describe('invokeGeneratedTool', () => { ); }); + /** Assert contact-card wire nesting survives the compatibility adapter and excludes the obsolete singular field. */ it('wraps send_contact.contact into contacts[] so the published SDK keeps the card', async () => { const client = createMockClient(); @@ -84,9 +87,9 @@ describe('invokeGeneratedTool', () => { ], }), ); - expect((client.messaging.sendContact as jest.Mock).mock.calls[0][1]).not.toHaveProperty( - 'contact', - ); + expect( + (client.messaging.sendContact as jest.Mock, unknown[]>).mock.calls[0]?.[1], + ).not.toHaveProperty('contact'); }); it('wraps send_carousel.cards into params', async () => { @@ -128,9 +131,9 @@ describe('invokeGeneratedTool', () => { const client = createMockClient(); await invokeGeneratedTool(client, 'list_settings', {}); - expect((client.channel as unknown as { listSettings: jest.Mock }).listSettings).toHaveBeenCalledWith( - 'test-api-token', - ); + expect( + (client.channel as unknown as { listSettings: jest.Mock }).listSettings, + ).toHaveBeenCalledWith('test-api-token'); await invokeGeneratedTool(client, 'create_settings', { webhookUrl: 'https://example.com/hook', diff --git a/src/__tests__/http.test.ts b/src/__tests__/http.test.ts index ea7e573..95bf176 100644 --- a/src/__tests__/http.test.ts +++ b/src/__tests__/http.test.ts @@ -1,4 +1,5 @@ import http from 'node:http'; + import { createHttpApp } from '../http'; describe('createHttpApp', () => { diff --git a/src/__tests__/json-schema-to-zod.test.ts b/src/__tests__/json-schema-to-zod.test.ts index 0570381..8b43796 100644 --- a/src/__tests__/json-schema-to-zod.test.ts +++ b/src/__tests__/json-schema-to-zod.test.ts @@ -7,7 +7,8 @@ describe('jsonSchemaToZod', () => { expect(createSettings).toBeDefined(); const schema = jsonSchemaToZod(createSettings!.inputSchema); expect( - (schema.parse({ webhookUrl: 'https://hooks.example/a' }) as { webhookUrl: string }).webhookUrl, + (schema.parse({ webhookUrl: 'https://hooks.example/a' }) as { webhookUrl: string }) + .webhookUrl, ).toBe('https://hooks.example/a'); expect( ( @@ -25,9 +26,8 @@ describe('jsonSchemaToZod', () => { expect((schema.parse({ mediaId: '1597399065095084' }) as { mediaId: string }).mediaId).toBe( '1597399065095084', ); - const fromNumber = ( - schema.parse({ mediaId: 1597399065095084 }) as { mediaId: string | number } - ).mediaId; + const fromNumber = (schema.parse({ mediaId: 1597399065095084 }) as { mediaId: string | number }) + .mediaId; expect(String(fromNumber)).toBe('1597399065095084'); }); }); diff --git a/src/__tests__/mock-sdk.ts b/src/__tests__/mock-sdk.ts index e648909..778e5ed 100644 --- a/src/__tests__/mock-sdk.ts +++ b/src/__tests__/mock-sdk.ts @@ -1,9 +1,9 @@ import { ClientConfig, type Client } from '@1msg/sdk'; export interface MockChatApiClientMocks { - sendMessage: jest.Mock; - getMe: jest.Mock; - listMessages: jest.Mock; + sendMessage: jest.Mock, unknown[]>; + getMe: jest.Mock, unknown[]>; + listMessages: jest.Mock, unknown[]>; } export interface MockChatApiClientBundle extends MockChatApiClientMocks { @@ -18,18 +18,18 @@ export function createMockChatApiClient(): MockChatApiClientBundle { token: 'test-token', }); - const sendMessage = jest.fn().mockResolvedValue({ + const sendMessage = jest.fn, unknown[]>().mockResolvedValue({ sent: true, id: 'wamid.mock.message', message: 'queued', }); - const getMe = jest.fn().mockResolvedValue({ + const getMe = jest.fn, unknown[]>().mockResolvedValue({ name: 'Test Channel', phone: '79001234567', }); - const listMessages = jest.fn().mockResolvedValue({ + const listMessages = jest.fn, unknown[]>().mockResolvedValue({ messages: [{ id: 'msg-1', body: 'hello' }], }); diff --git a/src/__tests__/polyfill-channel-settings.test.ts b/src/__tests__/polyfill-channel-settings.test.ts index 434da10..7678f1f 100644 --- a/src/__tests__/polyfill-channel-settings.test.ts +++ b/src/__tests__/polyfill-channel-settings.test.ts @@ -10,15 +10,23 @@ function jsonResponse(body: unknown, status = 200): Response { } as Response; } +/** Exercise compatibility request transformations with a recorded fake transport and restore global state afterward. */ describe('polyfillChannelSettings', () => { const calls: Array<{ url: string; init?: RequestInit }> = []; + /** Install a fresh request recorder per test so call assertions cannot inherit requests from another case. */ beforeEach(() => { calls.length = 0; - global.fetch = jest.fn(async (url: string | URL, init?: RequestInit) => { - calls.push({ url: String(url), init }); - return jsonResponse({ sent: true, path: String(url) }); - }) as typeof fetch; + /** Record URL and explicitly supplied options while returning an isolated successful JSON response. */ + global.fetch = jest.fn( + /** Record request input and return an isolated JSON response. */ async ( + url: string | URL, + init?: RequestInit, + ) => { + calls.push({ url: String(url), ...(init === undefined ? {} : { init }) }); + return jsonResponse({ sent: true, path: String(url) }); + }, + ) as typeof fetch; }); afterEach(() => { diff --git a/src/__tests__/rate-limit.test.ts b/src/__tests__/rate-limit.test.ts index 717e7b9..21e4f1e 100644 --- a/src/__tests__/rate-limit.test.ts +++ b/src/__tests__/rate-limit.test.ts @@ -1,8 +1,10 @@ import { RateLimiter, rateLimitKeyFromToken } from '../rate-limit'; +/** Use an injected clock to verify rate-limit boundaries without sleeping or depending on wall time. */ describe('RateLimiter', () => { + /** Assert the inclusive allowed-count boundary and rejection of the immediately following request. */ it('allows up to limit then rejects', () => { - let now = 1_000; + const now = 1_000; const limiter = new RateLimiter({ limit: 2, windowMs: 60_000, diff --git a/src/__tests__/sdk-only-architecture.test.ts b/src/__tests__/sdk-only-architecture.test.ts index a9ec69b..ffdd7da 100644 --- a/src/__tests__/sdk-only-architecture.test.ts +++ b/src/__tests__/sdk-only-architecture.test.ts @@ -1,5 +1,5 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; const MCP_SRC = path.resolve(__dirname, '..'); /** Hand-written sources that must not call Chat API over HTTP directly. */ @@ -25,10 +25,13 @@ const FORBIDDEN_HTTP_PATTERNS = [ /\bhttp\.request\s*\(/, ]; +/** Protect the MCP SDK boundary against a parallel hand-maintained HTTP client. */ describe('MCP SDK-only architecture', () => { + /** Inspect the package manifest to catch undeclared architecture drift independent of local node_modules. */ it('depends on @1msg/sdk and not on HTTP clients', () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const pkg = require('../../package.json') as { + const pkg = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8'), + ) as { dependencies?: Record; devDependencies?: Record; }; diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 71a31b5..9ab0e67 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1,15 +1,10 @@ import type { Client as SdkClient } from '@1msg/sdk'; -import { createMcpServer } from '../server'; // CJS subpath exports from @modelcontextprotocol/sdk -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { Client } = require('@modelcontextprotocol/sdk/client/index.js') as { - Client: typeof import('@modelcontextprotocol/sdk/client/index.js').Client; -}; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js') as { - InMemoryTransport: typeof import('@modelcontextprotocol/sdk/inMemory.js').InMemoryTransport; -}; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; + +import { createMcpServer } from '../server'; function createMockClient(): SdkClient { return { @@ -82,6 +77,7 @@ describe('createMcpServer tools/list', () => { }); }); +/** Exercise MCP dispatch through linked in-memory transports and close both sides after each scenario. */ describe('createMcpServer tools/call', () => { it('invokes send_message through MCP and returns JSON result', async () => { const mockClient = createMockClient(); @@ -113,6 +109,7 @@ describe('createMcpServer tools/call', () => { await server.close(); }); + /** Verify a void SDK result still produces valid textual MCP content required by consumers. */ it('returns JSON text when a tool result is void (Cursor requires content[].text)', async () => { const mockClient = createMockClient(); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -131,7 +128,7 @@ describe('createMcpServer tools/call', () => { expect(typeof content?.[0]?.text).toBe('string'); expect(content?.[0]?.text?.length).toBeGreaterThan(0); expect(JSON.parse(content?.[0]?.text as string)).toEqual( - expect.objectContaining({ error: expect.any(String) }), + expect.objectContaining({ error: expect.any(String) as unknown }), ); await client.close(); diff --git a/src/__tests__/tools-call.test.ts b/src/__tests__/tools-call.test.ts index fcd2e76..b4a4d02 100644 --- a/src/__tests__/tools-call.test.ts +++ b/src/__tests__/tools-call.test.ts @@ -1,16 +1,9 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -import type { Client as McpClientType } from '@modelcontextprotocol/sdk/client/index.js'; -import type { InMemoryTransport as InMemoryTransportType } from '@modelcontextprotocol/sdk/inMemory.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; + import { createMcpServer } from '../server'; import { createMockChatApiClient, type MockChatApiClientBundle } from './mock-sdk'; -const { Client } = require('@modelcontextprotocol/sdk/client') as { - Client: typeof McpClientType; -}; -const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js') as { - InMemoryTransport: typeof InMemoryTransportType; -}; - async function connectTestMcpClient(mock: MockChatApiClientBundle) { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const server = createMcpServer(mock.client); @@ -22,11 +15,13 @@ async function connectTestMcpClient(mock: MockChatApiClientBundle) { return { mcpClient, server }; } +/** Exercise public MCP calls using fake SDK capabilities and isolated linked transports. */ describe('createMcpServer tools/call', () => { afterEach(() => { jest.restoreAllMocks(); }); + /** Check normalized recipient arguments and message content through the public MCP client. */ it('dispatches send_message to ChatApiClient.sendMessage with normalized args', async () => { const mock = createMockChatApiClient(); const { mcpClient, server } = await connectTestMcpClient(mock); @@ -51,13 +46,14 @@ describe('createMcpServer tools/call', () => { expect(content[0]).toMatchObject({ type: 'text', }); - const text = content[0].text ?? ''; + const text = content[0]?.text ?? ''; expect(text).toContain('wamid.mock.message'); await mcpClient.close(); await server.close(); }); + /** Check profile dispatch uses the configured token and returns the SDK response as textual content. */ it('dispatches get_me to profile.getMe', async () => { const mock = createMockChatApiClient(); const { mcpClient, server } = await connectTestMcpClient(mock); @@ -70,13 +66,14 @@ describe('createMcpServer tools/call', () => { expect(mock.getMe).toHaveBeenCalledTimes(1); expect(mock.getMe).toHaveBeenCalledWith('test-token'); expect(result.isError).toBeFalsy(); - const text = (result.content as Array<{ text: string }>)[0].text; + const text = (result.content as Array<{ text: string }>)[0]?.text ?? ''; expect(text).toContain('Test Channel'); await mcpClient.close(); await server.close(); }); + /** Check list dispatch forwards authentication and returns messages through the public MCP transport. */ it('dispatches list_messages to messaging.listMessages', async () => { const mock = createMockChatApiClient(); const { mcpClient, server } = await connectTestMcpClient(mock); @@ -87,15 +84,16 @@ describe('createMcpServer tools/call', () => { }); expect(mock.listMessages).toHaveBeenCalledTimes(1); - expect(mock.listMessages.mock.calls[0][0]).toBe('test-token'); + expect(mock.listMessages.mock.calls[0]?.[0]).toBe('test-token'); expect(result.isError).toBeFalsy(); - const text = (result.content as Array<{ text: string }>)[0].text; + const text = (result.content as Array<{ text: string }>)[0]?.text ?? ''; expect(text).toContain('msg-1'); await mcpClient.close(); await server.close(); }); + /** Ensure invalid tool arguments produce MCP error content instead of an unintended SDK call. */ it('returns tool error content when required args are missing', async () => { const mock = createMockChatApiClient(); const { mcpClient, server } = await connectTestMcpClient(mock); @@ -107,7 +105,7 @@ describe('createMcpServer tools/call', () => { expect(mock.sendMessage).not.toHaveBeenCalled(); expect(result.isError).toBe(true); - const text = (result.content as Array<{ text: string }>)[0].text; + const text = (result.content as Array<{ text: string }>)[0]?.text ?? ''; expect(text).toContain('body'); await mcpClient.close(); diff --git a/src/auth.ts b/src/auth.ts index ab89f0a..cf00f54 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,9 +1,6 @@ import type { IncomingMessage } from 'node:http'; -import { - isClientSelectableApiHost, - normalizeBaseUrl, - type McpServerConfig, -} from './config'; + +import { isClientSelectableApiHost, normalizeBaseUrl, type McpServerConfig } from './config'; export class McpAuthError extends Error { readonly statusCode: number; @@ -15,10 +12,7 @@ export class McpAuthError extends Error { } } -function headerValue( - headers: IncomingMessage['headers'], - name: string, -): string | undefined { +function headerValue(headers: IncomingMessage['headers'], name: string): string | undefined { const raw = headers[name.toLowerCase()]; if (Array.isArray(raw)) { return raw[0]?.trim() || undefined; @@ -72,13 +66,9 @@ export interface HttpHostConfig { baseUrl: string; } -function extractBaseUrlOverride( - headers: IncomingMessage['headers'], -): string | undefined { - return ( - headerValue(headers, 'x-1msg-base-url') || - headerValue(headers, 'x-base-url') - ); +/** Read the explicit channel API override from environment settings without falling back to an unrelated credential field. */ +function extractBaseUrlOverride(headers: IncomingMessage['headers']): string | undefined { + return headerValue(headers, 'x-1msg-base-url') || headerValue(headers, 'x-base-url'); } /** Build per-request MCP/SDK config from host env + request headers. */ diff --git a/src/config.ts b/src/config.ts index f5d0bcf..70262ba 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,9 +29,7 @@ export function normalizeBaseUrl(raw: string): string { throw new Error('ONE_MSG_BASE_URL must not be empty.'); } - const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) - ? trimmed - : `https://${trimmed}`; + const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`; let parsed: URL; try { @@ -109,19 +107,12 @@ export function loadMcpConfig(): McpServerConfig { missing.push('ONE_MSG_BASE_URL'); } - const token = readEnv('ONE_MSG_TOKEN', [ - 'CHAT_API_TOKEN', - 'CHAT_API_KEY', - 'TOKEN', - ]); + const token = readEnv('ONE_MSG_TOKEN', ['CHAT_API_TOKEN', 'CHAT_API_KEY', 'TOKEN']); if (!token) { missing.push('ONE_MSG_TOKEN'); } - const instanceId = readEnv('ONE_MSG_INSTANCE_ID', [ - 'CHAT_API_INSTANCE_ID', - 'INSTANCE_ID', - ]); + const instanceId = readEnv('ONE_MSG_INSTANCE_ID', ['CHAT_API_INSTANCE_ID', 'INSTANCE_ID']); if (!instanceId) { missing.push('ONE_MSG_INSTANCE_ID'); } diff --git a/src/http.ts b/src/http.ts index bb2eb41..e882d37 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,37 +1,25 @@ #!/usr/bin/env node - -import { createHash, randomUUID } from 'node:crypto'; -import type { Request, Response } from 'express'; import type { McpServer as McpServerType } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { StreamableHTTPServerTransport as StreamableHTTPServerTransportType } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { Request, Response } from 'express'; + import { Client } from '@1msg/sdk'; +import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { createHash, randomUUID } from 'node:crypto'; + import { McpAuthError, resolveRequestConfig } from './auth'; import { normalizeBaseUrl, toClientConfig } from './config'; import { hashIdentifier, logRequest } from './logging'; import { RateLimiter, rateLimitKeyFromToken } from './rate-limit'; import { createMcpServer } from './server'; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js') as { - StreamableHTTPServerTransport: typeof StreamableHTTPServerTransportType; -}; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { isInitializeRequest } = require('@modelcontextprotocol/sdk/types.js') as { - isInitializeRequest: (value: unknown) => boolean; -}; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { createMcpExpressApp } = require('@modelcontextprotocol/sdk/server/express.js') as { - createMcpExpressApp: (options?: { - host?: string; - allowedHosts?: string[]; - }) => import('express').Express; -}; - const PACKAGE_NAME = '@1msg/mcp'; const VERSION = '1.2.8'; interface SessionEntry { - transport: StreamableHTTPServerTransportType; + transport: StreamableHTTPServerTransport; server: McpServerType; instanceIdHash: string; tokenHash: string; @@ -54,6 +42,7 @@ function readEnvInt(name: string, fallback: number): number { return Number.isFinite(value) && value > 0 ? value : fallback; } +/** Validate HTTP bootstrap settings and normalize the fixed upstream base URL; omit unspecified host restrictions. */ function loadHttpOptionsFromEnv(): HttpServerOptions { const baseUrl = process.env.ONE_MSG_BASE_URL?.trim() || @@ -61,9 +50,7 @@ function loadHttpOptionsFromEnv(): HttpServerOptions { process.env.CHAT_API_ROOT_URL?.trim() || process.env.CHAT_API_URL?.trim(); if (!baseUrl) { - throw new Error( - 'ONE_MSG_BASE_URL is required for HTTP mode (fixed upstream API root).', - ); + throw new Error('ONE_MSG_BASE_URL is required for HTTP mode (fixed upstream API root).'); } const allowedHosts = process.env.MCP_ALLOWED_HOSTS?.split(',') @@ -75,7 +62,7 @@ function loadHttpOptionsFromEnv(): HttpServerOptions { port: readEnvInt('MCP_HTTP_PORT', 3100), host: process.env.MCP_HTTP_HOST?.trim() || '0.0.0.0', rateLimitRpm: readEnvInt('MCP_RATE_LIMIT_RPM', 60), - allowedHosts: allowedHosts?.length ? allowedHosts : undefined, + ...(allowedHosts?.length ? { allowedHosts } : {}), }; } @@ -104,7 +91,7 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp const app = createMcpExpressApp({ host: options.host, - allowedHosts: options.allowedHosts, + ...(options.allowedHosts === undefined ? {} : { allowedHosts: options.allowedHosts }), }); app.get('/healthz', (_req, res) => { @@ -120,6 +107,7 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp res.status(200).json({ ok: true }); }); + /** Authenticate the request, resolve or recreate its session, enforce the rate limit and delegate to the MCP transport. */ const handleMcp = async (req: Request, res: Response): Promise => { const requestId = randomUUID(); const started = Date.now(); @@ -128,10 +116,7 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp try { let credentials; try { - credentials = resolveRequestConfig( - { baseUrl: options.baseUrl }, - req.headers, - ); + credentials = resolveRequestConfig({ baseUrl: options.baseUrl }, req.headers); } catch (error) { if (error instanceof McpAuthError) { logRequest({ @@ -171,8 +156,7 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp return; } - let entry: SessionEntry | undefined = - sessionHeader ? sessions.get(sessionHeader) : undefined; + let entry: SessionEntry | undefined = sessionHeader ? sessions.get(sessionHeader) : undefined; if (entry) { if (entry.tokenHash !== tokenKey) { @@ -205,7 +189,9 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp } }; - await server.connect(transport); + // The SDK implementation permits undefined callbacks while its Transport + // interface marks them optional. Both refer to the same runtime contract. + await server.connect(transport as Transport); entry = { transport, server, @@ -228,10 +214,7 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp sendJson( res, 400, - mcpError( - -32000, - 'Missing mcp-session-id. Send an initialize request first.', - ), + mcpError(-32000, 'Missing mcp-session-id. Send an initialize request first.'), ); return; } else { @@ -239,10 +222,10 @@ export function createHttpApp(options: HttpServerOptions): import('express').Exp // stateless transport so tools/call still works on a different LB. const client = new Client(toClientConfig(credentials)); const server = createMcpServer(client); - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - }); - await server.connect(transport); + const transport = new StreamableHTTPServerTransport({}); + // The SDK implementation permits undefined callbacks while its Transport + // interface marks them optional. Both refer to the same runtime contract. + await server.connect(transport as Transport); const closeFailover = () => { void transport.close(); }; @@ -364,10 +347,9 @@ async function main(argv: string[]): Promise { } if (require.main === module) { + /** Report bootstrap failure and return a failing process status to the supervising runtime. */ main(process.argv.slice(2)).catch((error) => { - process.stderr.write( - `${error instanceof Error ? error.message : String(error)}\n`, - ); + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; }); } diff --git a/src/json-schema-to-zod.ts b/src/json-schema-to-zod.ts index 224f041..5c09c58 100644 --- a/src/json-schema-to-zod.ts +++ b/src/json-schema-to-zod.ts @@ -1,24 +1,32 @@ import { z } from 'zod'; + import type { JsonSchema } from './tools.generated'; +/** Build a union from each supported schema alternative; an empty alternatives list accepts unknown data. */ function unionFromSchemas(schemas: JsonSchema[]): z.ZodTypeAny { const parts = schemas.map((item) => jsonPropertyToZod(item)); if (parts.length === 0) { return z.unknown(); } if (parts.length === 1) { - return parts[0]; + return parts[0] ?? z.unknown(); } return z.union(parts as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]); } +/** Translate supported generated property constraints into Zod validators while retaining optional array item semantics. */ function jsonPropertyToZod(schema: JsonSchema): z.ZodTypeAny { if (schema.enum && schema.enum.length > 0) { const values = schema.enum.filter((value): value is string | number => { return typeof value === 'string' || typeof value === 'number'; }); if (values.length > 0) { - return z.union(values.map((value) => z.literal(value)) as [z.ZodLiteral, ...z.ZodLiteral[]]); + return z.union( + values.map((value) => z.literal(value)) as [ + z.ZodLiteral, + ...z.ZodLiteral[], + ], + ); } } @@ -28,7 +36,13 @@ function jsonPropertyToZod(schema: JsonSchema): z.ZodTypeAny { } if (Array.isArray(schema.type) && schema.type.length > 1) { - return unionFromSchemas(schema.type.map((type) => ({ type, items: schema.items }))); + return unionFromSchemas( + /** Keep array metadata only when present so exact optional properties remain valid. */ + schema.type.map((type) => ({ + type, + ...(schema.items === undefined ? {} : { items: schema.items }), + })), + ); } const type = Array.isArray(schema.type) ? schema.type[0] : schema.type; diff --git a/src/logging.ts b/src/logging.ts index df305b9..5824d77 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -31,7 +31,7 @@ export function redactForLog(value: unknown, depth = 0): unknown { if (depth > 6) { return '[truncated]'; } - if (value == null) { + if (value === null || value === undefined) { return value; } if (typeof value === 'string') { diff --git a/src/polyfill-channel-settings.ts b/src/polyfill-channel-settings.ts index ce13216..02b05a8 100644 --- a/src/polyfill-channel-settings.ts +++ b/src/polyfill-channel-settings.ts @@ -41,6 +41,7 @@ async function postJsonSafe( return readJson(response); } +/** Normalize SDK contact-name aliases to the API wire keys while preserving additional caller-supplied fields. */ function contactNameForApi(name: unknown): unknown { if (typeof name === 'string') { return { formatted_name: name, first_name: name.split(/\s+/)[0] }; @@ -53,12 +54,12 @@ function contactNameForApi(name: unknown): unknown { const first = raw.first_name ?? raw.firstName ?? - (formatted != null ? String(formatted).split(/\s+/)[0] : undefined); + (formatted !== null && formatted !== undefined ? String(formatted).split(/\s+/)[0] : undefined); const last = raw.last_name ?? raw.lastName; const out: Record = {}; - if (formatted != null) out.formatted_name = formatted; - if (first != null) out.first_name = first; - if (last != null) out.last_name = last; + if (formatted !== null && formatted !== undefined) out.formatted_name = formatted; + if (first !== null && first !== undefined) out.first_name = first; + if (last !== null && last !== undefined) out.last_name = last; for (const [key, value] of Object.entries(raw)) { if (['formattedName', 'firstName', 'lastName', 'middleName'].includes(key)) { continue; @@ -68,13 +69,15 @@ function contactNameForApi(name: unknown): unknown { return out; } +/** Normalize each object contact name without mutating the caller payload; non-object inputs pass through unchanged. */ function sendContactPayload(body: unknown): unknown { if (!body || typeof body !== 'object') { return body; } const payload = { ...(body as Record) }; if (Array.isArray(payload.contacts)) { - payload.contacts = payload.contacts.map((item) => { + /** Normalize object contacts individually while leaving unknown primitive entries available for upstream validation. */ + payload.contacts = payload.contacts.map((item: unknown) => { if (!item || typeof item !== 'object') return item; const contact = { ...(item as Record) }; contact.name = contactNameForApi(contact.name); @@ -117,6 +120,22 @@ function sendCarouselPayload( return payload; } +/** Delete canonical media first; only a non-success response activates the supported legacy endpoint. */ +async function deleteMediaRequest( + buildUrl: (path: string) => string, + mediaId: string, +): Promise { + const response = await fetch(buildUrl(`/media/${encodeURIComponent(mediaId)}`), { + method: 'DELETE', + }); + if (response.ok) { + const text = await response.text(); + const result: unknown = text ? JSON.parse(text) : { result: 'success' }; + return result; + } + return postJsonSafe(buildUrl, '/deleteMedia', { mediaId }); +} + /** * GET/POST /settings exist on the live API but were missing from public OpenAPI, * so older @1msg/sdk builds have no ChannelApi.listSettings / createSettings. @@ -178,17 +197,9 @@ export function polyfillChannelSettings(client: ChannelSettingsClient): void { ) => Promise; }; - messaging.deleteMedia = async (first: string, second?: string) => { - const mediaId = pickNumericMediaId(first, second); - const response = await fetch(buildUrl(`/media/${encodeURIComponent(mediaId)}`), { - method: 'DELETE', - }); - if (response.ok) { - const text = await response.text(); - return text ? JSON.parse(text) : { result: 'success' }; - } - return postJsonSafe(buildUrl, '/deleteMedia', { mediaId }); - }; + /** Resolve the media identifier across both supported SDK argument orders before running the deletion scenario. */ + messaging.deleteMedia = (first: string, second?: string) => + deleteMediaRequest(buildUrl, pickNumericMediaId(first, second)); messaging.createUploadMedia = async (_token: string, body?: unknown) => { const payload: Record = diff --git a/src/server.ts b/src/server.ts index cf8aaf3..6c54faa 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,22 +1,15 @@ -import type { McpServer as McpServerType } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { StdioServerTransport as StdioServerTransportType } from '@modelcontextprotocol/sdk/server/stdio.js'; import { Client } from '@1msg/sdk'; +// CJS runtime requires explicit .js subpath exports from the MCP SDK package. +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; + import type { McpServerConfig } from './config'; + import { toClientConfig } from './config'; import { invokeGeneratedTool } from './handlers.generated'; -import { GENERATED_MCP_TOOLS } from './tools.generated'; import { jsonSchemaToZod } from './json-schema-to-zod'; import { polyfillChannelSettings } from './polyfill-channel-settings'; - -// CJS runtime requires explicit .js subpath exports from the MCP SDK package. -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js') as { - McpServer: typeof McpServerType; -}; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js') as { - StdioServerTransport: typeof StdioServerTransportType; -}; +import { GENERATED_MCP_TOOLS } from './tools.generated'; const SERVER_NAME = '@1msg/mcp'; const SERVER_VERSION = '1.2.8'; @@ -30,7 +23,8 @@ function serializeToolResult(result: unknown): string { // Published @1msg/sdk createUploadMedia is void and used to hit this. if (result === undefined) { return JSON.stringify({ - error: 'API returned no JSON body. Hosted MCP polyfills create_upload_media; use url or body.', + error: + 'API returned no JSON body. Hosted MCP polyfills create_upload_media; use url or body.', }); } return JSON.stringify(result, null, 2); @@ -62,7 +56,7 @@ function formatToolError(error: unknown): string { } /** Create MCP server with all generated Chat API tools. */ -export function createMcpServer(client: Client): McpServerType { +export function createMcpServer(client: Client): McpServer { polyfillChannelSettings(client); const server = new McpServer({ name: SERVER_NAME, @@ -110,7 +104,7 @@ export function createMcpServer(client: Client): McpServerType { } /** Start MCP stdio server using SDK client derived from env config. */ -export async function startMcpServer(config: McpServerConfig): Promise { +export async function startMcpServer(config: McpServerConfig): Promise { const client = new Client(toClientConfig(config)); const server = createMcpServer(client); const transport = new StdioServerTransport(); diff --git a/src/tools.generated.ts b/src/tools.generated.ts index 4c9c489..9342133 100644 --- a/src/tools.generated.ts +++ b/src/tools.generated.ts @@ -33,25 +33,32 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "add_template", "operationId": "addTemplate", - "description": "Create message template", + "description": "Create a WABA message template (`name`, `category`, `language`, `components`).\nAfter create, Meta reviews the template; list it with `GET /templates`.\n", "inputSchema": { "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "Template name", + "example": "hello_world" }, "category": { - "type": "string" + "type": "string", + "description": "MARKETING, UTILITY, or AUTHENTICATION", + "example": "UTILITY" }, "language": { - "type": "string" + "type": "string", + "description": "Template language code (e.g. en_US)", + "example": "en_US" }, "components": { "type": "array", "items": { "type": "object", "additionalProperties": true - } + }, + "description": "Template components (HEADER, BODY, FOOTER, BUTTONS, CAROUSEL)" } }, "additionalProperties": false @@ -339,7 +346,7 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "create_settings", "operationId": "createSettings", - "description": "Update channel settings. Only send fields you want to change.\n\n`webhookUrl` **replaces** the stored list (one URL string, or up to 5 URLs as an array).\nPassing an empty value clears webhooks.\n", + "description": "Update channel settings. Only send fields you want to change.\n\n`webhookUrl` **replaces** the stored list (one URL string, or up to 5 URLs as an array).\nPassing an empty value clears webhooks. Repeating the same URL(s) also clears a\nsticky auto-disable. With several URLs, send the complete array to re-enable one\nof them — omitting a URL removes it from the channel.\n", "inputSchema": { "type": "object", "properties": { @@ -460,11 +467,11 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ "type": "object", "properties": { "mediaId": { - "oneOf": [ - { "type": "string" }, - { "type": "integer" } + "type": [ + "string", + "number" ], - "description": "Numeric WABA media id (string or number)" + "description": "Numeric WABA media id" } }, "additionalProperties": false, @@ -628,7 +635,7 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "get_me", "operationId": "getMe", - "description": "Get WhatsApp Business Account profile information", + "description": "Return the WhatsApp Business profile for this channel: About, address,\ndescription, email, phone, websites, industry (`vertical`), and profile photo URL.\n", "inputSchema": { "type": "object", "properties": {}, @@ -650,7 +657,7 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "get_status", "operationId": "getStatus", - "description": "Returns WhatsApp Business API client connection status.", + "description": "Return WhatsApp Cloud API connection status for this channel (`connected`\nwhen the number can send). May include `accountStatus`, messaging limit,\nand MM Lite availability.\n\nOptional `health_status_warning` appears only when Meta Graph health could\nnot be loaded and a soft `connected` response is returned. Omitted on the\nnormal path and when health details are not applicable for the channel —\nnever an internal provider/transport name.\n", "inputSchema": { "type": "object", "properties": {}, @@ -661,7 +668,7 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "get_webhook", "operationId": "getWebhook", - "description": "Returns the configured client webhook URL for this channel.", + "description": "Return the webhook URL(s) currently configured for this channel, plus delivery\nstatus for each URL.\n\n`webhookUrl` is a string when a single URL is set, an array of strings when\nmultiple URLs are configured, or `null` if none is set.\n\n**Prefer `webhookStatuses`.** The circuit breaker is per URL, not per channel.\nIf 1 of 4 URLs is sticky-disabled, that URL is skipped; the other three still\nreceive events. Send/receive are not affected.\n\nChannel-level `haveDisabledWebhooks`, `circuitState`, `reason`, `fails`,\n`trips`, and timestamps are a compatibility summary of **one** URL (the first\nsticky-disabled URL, else the first configured URL). They are not \"the channel\nis dead\".\n\n- `haveDisabledWebhooks` — `true` when **at least one** URL in `webhookUrl` is\n sticky auto-disabled. Live URLs still get events.\n- `circuitState` — compressed scalar for that same primary URL:\n `ok` | `degraded` | `open` | `half_open` | `disabled`.\n- `disabledWebhooks` — currently configured URLs that are sticky-disabled\n (subset of `webhookUrl`).\n- `webhookStatuses` — one row per currently configured URL. Use this array\n to see which endpoints are live vs skipped.\n- `fails` / `trips` / `firstFailAt` — **diagnostics for support**, not product\n UI. `fails` = failures in a rolling ~10 minute window (not lifetime).\n `trips` = failed recovery probes on the 15 min → 1 h → 6 h schedule (sticky\n disable after 3). Clients can ignore these and use `webhookStatuses` +\n `disabledWebhooks` only.\n\nSticky disable skips delivery; it does not unconfigure the URL (it stays in\n`webhookUrl`). Re-enable with `POST /webhook` or `POST /settings`. Those\nendpoints **replace** the whole list — with multiple URLs, send the full array.\n", "inputSchema": { "type": "object", "properties": {}, @@ -683,7 +690,7 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "initiate_call", "operationId": "initiateCall", - "description": "Perform a WhatsApp Calling action (beta).\n\nProxies upstream `POST /calling/calls`. Despite the historical path\nname `/initiateCall`, this endpoint handles **all** call actions:\n\n| action | Use | Required |\n|--------|-----|----------|\n| `connect` | Outbound business → user | `to` + `session` (`sdp_type: offer`) |\n| `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + `session` (`sdp_type: answer`) |\n| `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) |\n| `reject` | Decline inbound | `call_id` |\n| `terminate` | Hang up | `call_id` |\n\n**SDP / media (critical)**\n- `accept` / `pre_accept` require a **WebRTC-generated SDP answer**.\n- Do **not** send Meta's offer SDP back as the answer.\n- Postman (or curl) alone **cannot** establish real media — you need a\n WebRTC or SIP stack. 1msg only proxies signaling.\n\nAnswer within ~**30–60 seconds** of an inbound `connect` webhook or Meta\nterminates as unanswered. Common Meta errors include Calling not enabled\n(`138000`), no permission (`138006`), SDP validation failures.\n\n**Outbound** requires a prior Call Permission Request (CPR) acceptance.\nSee the **Calling** tag overview for the full outbound flow and CPR limits.\n\nTrial / `subscriptionBlocked` → **403** plain text.\nUpstream failures often return HTTP 200 with `{ \"response\": { \"error\": \"...\" } }`.\n", + "description": "Perform a WhatsApp Calling action (beta).\n\nProxies upstream `POST /calling/calls`. Despite the historical path\nname `/initiateCall`, this endpoint handles **all** call actions:\n\n| action | Use | Required |\n|--------|-----|----------|\n| `connect` | Outbound business → user | `to` + `session` (`sdp_type: offer`) |\n| `pre_accept` | Inbound (optional, reduces audio clipping) | `call_id` + `session` (`sdp_type: answer`) |\n| `accept` | Inbound answer | `call_id` + `session` (`sdp_type: answer`) |\n| `reject` | Decline inbound | `call_id` |\n| `terminate` | Hang up | `call_id` |\n\n**SDP / media (critical)**\n- `accept` / `pre_accept` require a **WebRTC-generated SDP answer**.\n- Do **not** send Meta's offer SDP back as the answer.\n- Postman (or curl) alone **cannot** establish real media — you need a\n WebRTC or SIP stack. 1msg only proxies signaling.\n\nAnswer within ~**30–60 seconds** of an inbound `connect` webhook or Meta\nterminates as unanswered. Common Meta errors include Calling not enabled\n(`138000`), no permission (`138006`), SDP validation failures.\n\n**Outbound** requires a prior Call Permission Request (CPR) acceptance.\nSee the **Calling** tag overview for the full outbound flow and CPR limits.\n\nTrial / `subscriptionBlocked` → **403** plain text.\nUpstream failures often return HTTP 200 with `{ \"response\": { \"error\": \"\" } }`.\n`Invalid API KEY` is used only when Meta/360 body actually indicates an\ninvalid API key or token. Other upstream 401s (notably call permission\n`138006`) pass through `error.message` / `error_user_msg`.\n", "inputSchema": { "type": "object", "properties": { @@ -879,7 +886,7 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "list_settings", "operationId": "listSettings", - "description": "Return channel settings: webhook URL(s), delivery flags, and analytics toggles.\n\nThis is the same `GET /settings` endpoint as the REST API (previously missing from MCP).\n", + "description": "Return channel settings: webhook URL(s), delivery flags, and analytics toggles.\n\nAuto-disable / circuit state matches `GET /webhook`. **Prefer `webhookStatuses`**\n(one row per configured URL). The breaker is per URL, not per channel.\n\nChannel-level `haveDisabledWebhooks` is `true` when **at least one** URL in\n`webhookUrl` is sticky auto-disabled — not \"the channel is dead\". Live URLs\nstill receive events. Top-level `circuitState` / `reason` / `fails` / `trips`\nare a compatibility summary of one URL (first sticky-disabled, else first\nconfigured).\n\nThis is the same `GET /settings` endpoint as the REST API (previously missing from MCP).\n", "inputSchema": { "type": "object", "properties": {}, @@ -2268,26 +2275,32 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ { "name": "set_webhook", "operationId": "setWebhook", - "description": "Set the client webhook URL for inbound events.\n\n**Replaces** the stored webhook list with this single URL (it does not append).\nTo send to several endpoints, pass an array in `webhookUrl` or use `POST /settings`.\n\nWhatsApp **Calling** events (`field=calls`) are forwarded as passthrough\npayloads with `type: \"calls\"` and `instanceId` (connect / status / terminate).\nCall permission replies arrive on the normal messages path\n(`call_permission_reply`). Details: **Calling** tag.\n", + "description": "Set the client webhook URL that receives inbound events for this channel.\n\n**Replaces** the stored webhook list (it does not append). To deliver the same\nevents to several endpoints, pass an array in `webhookUrl` (max 5) or use\n`POST /settings`. With several URLs already configured, posting only the\ndisabled URL **drops the others** — send the complete array to re-enable one\nof many.\n\nIncoming payloads include messages, delivery ACKs, group updates, coexistence\nechoes, and calling events. WhatsApp Calling payloads (`type: \"calls\"`) are\ndocumented under the **Calling** tag. Call permission replies arrive on the\nnormal messages path (`call_permission_reply`).\n\nRepeating `POST /webhook` with the same URL(s) clears an auto-disable (self-serve\nre-enable). Send/receive are not affected by webhook auto-disable.\n", "inputSchema": { "type": "object", "properties": { "webhookUrl": { "oneOf": [ { - "type": "string" + "type": "string", + "format": "uri" }, { "type": "array", "items": { - "type": "string" - } + "type": "string", + "format": "uri" + }, + "maxItems": 5 } ], - "description": "Replacement webhook URL, or array of URLs (max 5)" + "description": "HTTP or HTTPS URL, or array of URLs (max 5). Replaces the stored list.\nEmpty values are rejected — clear webhooks with `POST /settings`.\n" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "webhookUrl" + ] }, "inputFields": [ "webhookUrl" @@ -2508,17 +2521,21 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ "properties": { "about": { "type": "string", - "description": "Profile About section" + "description": "Profile About section (WhatsApp status text)", + "example": "Available 9–18" }, "address": { - "type": "string" + "type": "string", + "description": "Business address shown on the profile" }, "description": { "type": "string", "description": "Business description (max 256 characters)" }, "email": { - "type": "string" + "type": "string", + "description": "Business email shown on the profile", + "example": "support@example.com" }, "vertical": { "type": "string", @@ -2532,7 +2549,8 @@ export const GENERATED_MCP_TOOLS: GeneratedMcpTool[] = [ "type": "array", "items": { "type": "string" - } + }, + "description": "Business website URLs" } }, "additionalProperties": false diff --git a/tools/quality/README.md b/tools/quality/README.md new file mode 100644 index 0000000..ebb38d7 --- /dev/null +++ b/tools/quality/README.md @@ -0,0 +1,85 @@ +# 1MSG quality template 1.0.2 + +Vendored, private template for 1MSG-156. Install its exact tools with +`npm --prefix tools/quality ci` on Node 24.19.0. The lockfile and +`template-manifest.json` identify the reviewed version. This package is not +published to npm. Each consumer commits a full copy, its manifest, and its own +`quality.config.mjs` inventory. A version update requires policy review and +real negative fixtures; do not edit/reseal one consumer to hide a violation. + +## Profiles and commands + +A (Node runtime) and B (UI) require full static and typed ESLint, strict production, +test and tooling typechecks, architecture, debt gates, tests, and production build. +D applies these to authored TypeScript/tooling while each generated language uses +reproducible generation and native build/offline contract tests. C runs formatting +and static lint on every changed/new authored file and ratchets existing debt. +Compiler presets are provided for NodeNext, Next bundler and Svelte bundler; +consumers must include real sources/tests/tooling and the framework's generated types. + +Exported `staticConfig`, `typedConfig`, `maintainabilityConfig` compose with the +actual framework preset. Next uses its compatible core-web-vitals + TypeScript +presets and React Hooks; Svelte uses its parser/plugin and `svelte-check` on real +components. Browser globals and Node runtime checks have separate scopes. + +`node tools/quality/run.mjs guards` checks source inventory, suppressions, vendored +integrity and documentation for changed functions/private methods/substantive +callbacks plus full pilot roots. Comments explain purpose, effects and constraints; +AST coverage is mechanical, usefulness and accuracy require review. Formatting-only +changes do not create a method change. Meaningless boilerplate does not satisfy review. + +Mandatory graph/debt commands: `unused-production`, `unused-full`, `duplicates`. +`duplicates-full` saves the extended tests/fixtures report and fails analyzer errors; +it is a report, not the runtime clone gate. Configure `duplicatePaths`, +`duplicateFullPaths`, exact generated ignores, explicit formats, and an inventoried +numeric `duplicateMaxPercentage`. Detection uses minLines=5, minTokens=50, mild mode. +Clone groups cannot grow even if the overall duplicate percentage falls. + +Wrap actual JSON analyzer output with: + +- `report --kind eslint --name complexity -- node tools/quality/node_modules/eslint/bin/eslint.js . -c eslint.complexity.config.mjs -f json` +- `report --kind depcruiser --name architecture -- node tools/quality/node_modules/dependency-cruiser/bin/dependency-cruiser.mjs src --config dependency-cruiser.config.cjs --output-type json` + +Only maintainability metrics and existing runtime cycles are baselinable through +those report commands. Other static/typed/architecture diagnostics remain fatal. +Knip keeps separate production and full graphs; missing resolvers, empty graphs, +stale exclusions and malformed output are failures. Configure public exports and +dynamic entrypoints explicitly, never declare all source as entrypoints. + +## Historical debt + +Initial bootstrap is local only and requires owner, expiry, reason and issue: +`--bootstrap --owner TEAM --expires YYYY-MM-DD --reason TEXT --issue URL`. +It reruns the actual analyzer against the trusted Git base using the adopted +configuration. New files, disconnected imports and increased findings cannot be +blessed as historical debt. Replay removes only newly added entry/project patterns +that have no historical source; existing source always comes from Git archive. +Dependencies are shared read-only with the current installation for resolution. + +CI supplies the trusted PR base SHA through `QUALITY_BASE` and fetches full history. +After adoption, allowances cannot grow or renew their expiry; reductions and removed +findings must retire allowances. Reports include head/base SHAs and gate outcome. +Never describe a nonempty baseline as a clean codebase. + +## CI and local hooks + +Run `npm --prefix tools/quality test` as a mandatory independent check. Fixtures +execute actual Knip, jscpd and dependency-cruiser CLIs and verify ratchet growth, +coverage, documentation and malformed reports. Consumers add real typed/framework, +public API, native I/O, SQL, compatibility and generated-drift fixtures as applicable. +`Quality` / `quality-required` must aggregate all applicable checks and fail on any +missing, failed or cancelled required job. Configure this actual job context in +branch protection without removing existing requirements. CI never runs `--fix`. + +The pinned Husky/lint-staged dependencies support committed `.husky/pre-commit` and +an explicit repository `hooks:install` command. Keep hooks quick; CI is authoritative. +Do not change shared Git hook configuration of another worktree during verification. + +## Updating this template + +Change the template at its source, update its version, test actual consumers, +run `node seal.mjs`, copy the complete template, and commit lockfiles and inventory. +Integrity verification prevents hiding authored code under the vendor exemption. +No analyzer flag may depend on CI or a skip-type-aware environment variable. + +Version 1.0.2 resolves Svelte embedded clone paths to their physical source while retaining fragment identity. Initial Knip replay omits only the exact current-only sealed vendor ignore; authored stale ignores remain errors. diff --git a/tools/quality/eslint.mjs b/tools/quality/eslint.mjs new file mode 100644 index 0000000..142b9f1 --- /dev/null +++ b/tools/quality/eslint.mjs @@ -0,0 +1,137 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import imports from 'eslint-plugin-import'; +import node from 'eslint-plugin-n'; +import unicorn from 'eslint-plugin-unicorn'; +import perfectionist from 'eslint-plugin-perfectionist'; +import sonarjs from 'eslint-plugin-sonarjs'; +import globals from 'globals'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +export { + tseslint, + imports as importPlugin, + node as nodePlugin, + sonarjs, + unicorn, + perfectionist, + globals, +}; + +export const sourceFiles = ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}']; +export const testFiles = [ + '**/*.{test,spec,stories}.{js,jsx,ts,tsx}', + '**/{test,tests,__tests__,fixtures,mocks}/**', +]; + +/** Return the shared correctness/style layer; callers provide explicit browser/Node globals and generated paths. */ +export function staticConfig({ files = sourceFiles, globals = {} } = {}) { + return [ + { + files, + languageOptions: { globals }, + linterOptions: { reportUnusedDisableDirectives: 'error' }, + plugins: { import: imports, n: node, unicorn, perfectionist, sonarjs }, + settings: { + 'import/resolver': { + [require.resolve('eslint-import-resolver-typescript')]: { + alwaysTryTypes: true, + }, + node: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'] }, + }, + }, + rules: { + ...js.configs.recommended.rules, + eqeqeq: ['error', 'always'], + 'no-var': 'error', + 'prefer-const': 'error', + 'import/no-unresolved': 'error', + 'import/no-duplicates': 'error', + 'n/no-unsupported-features/node-builtins': [ + 'error', + { version: '>=24.0.0 <25.0.0' }, + ], + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/prefer-type-error': 'error', + 'perfectionist/sort-imports': [ + 'error', + { type: 'natural', order: 'asc', sortSideEffects: false }, + ], + 'perfectionist/sort-exports': [ + 'error', + { type: 'natural', order: 'asc' }, + ], + 'sonarjs/no-identical-functions': 'error', + 'sonarjs/no-all-duplicated-branches': 'error', + }, + }, + ...tseslint.configs.recommended.map((config) => ({ + ...config, + files: ['**/*.{ts,tsx,cts,mts}'], + })), + ]; +} + +/** Apply mandatory type-aware Promise and unknown-boundary checks using actual repository tsconfigs. */ +export function typedConfig({ + files = ['**/*.{ts,tsx,cts,mts}'], + project, + tsconfigRootDir, +} = {}) { + return [ + { + files, + languageOptions: { + parser: tseslint.parser, + parserOptions: { project, tsconfigRootDir }, + }, + plugins: { '@typescript-eslint': tseslint.plugin }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + }, + }, + ]; +} + +/** Keep complexity diagnostics separate so only measured maintainability debt can use a ratchet. */ +export function maintainabilityConfig({ files = sourceFiles } = {}) { + return [ + { + files, + ignores: testFiles, + plugins: { sonarjs }, + rules: { + 'max-lines': [ + 'error', + { max: 500, skipBlankLines: true, skipComments: true }, + ], + 'max-lines-per-function': [ + 'error', + { max: 80, skipBlankLines: true, skipComments: true }, + ], + complexity: ['error', { max: 20, variant: 'classic' }], + 'sonarjs/cognitive-complexity': ['error', 15], + 'max-depth': ['error', 4], + 'max-params': ['error', 5], + }, + }, + { + files: ['**/*.{jsx,tsx}'], + ignores: testFiles, + rules: { + 'max-lines-per-function': [ + 'error', + { max: 150, skipBlankLines: true, skipComments: true }, + ], + }, + }, + ]; +} diff --git a/tools/quality/lib/baseline.mjs b/tools/quality/lib/baseline.mjs new file mode 100644 index 0000000..b4e11a4 --- /dev/null +++ b/tools/quality/lib/baseline.mjs @@ -0,0 +1,82 @@ +import { createHash } from 'node:crypto'; + +/** Hash normalized evidence so line moves cannot grant new duplicate allowances. */ +export function digest(text) { + return createHash('sha256').update(text).digest('hex'); +} + +/** Keep a stable identifier for each independent finding, rather than a global failure count. */ +export function record(key, value = 1, details = {}) { + if (!key || !Number.isFinite(value) || value < 0) + throw new Error('Invalid analyzer finding'); + return { key, value, ...details }; +} + +/** Reject duplicate diagnostic identities because collapsing them could hide growing debt. */ +export function index(records) { + const result = new Map(); + for (const entry of records) { + if (result.has(entry.key)) + throw new Error(`Duplicate finding identity: ${entry.key}`); + result.set(entry.key, entry); + } + return result; +} + +/** Compare current findings with allowances and the trusted base's allowances, including stale debt. */ +export function assess( + findings, + baseline, + previous, + today = new Date().toISOString().slice(0, 10), +) { + const errors = []; + if (baseline.schemaVersion !== 1 || !Array.isArray(baseline.entries)) { + throw new Error('Unsupported or missing baseline schema'); + } + const current = index(findings); + const allowances = index(baseline.entries); + const older = previous ? index(previous.entries) : null; + for (const [key, allowance] of allowances) { + if ( + !allowance.owner || + !allowance.reason || + !allowance.issue || + !allowance.expires + ) { + errors.push(`Unowned/unexplained allowance: ${key}`); + } + if ( + !/^\d{4}-\d{2}-\d{2}$/.test(allowance.expires ?? '') || + allowance.expires < today + ) { + errors.push(`Expired or invalid allowance: ${key}`); + } + const finding = current.get(key); + if (!finding) errors.push(`Remove stale allowance: ${key}`); + else if (finding.value < allowance.value) + errors.push(`Reduce allowance to ${finding.value}: ${key}`); + if (older) { + const old = older.get(key); + if (!old || allowance.value > old.value) + errors.push(`Baseline expanded relative to trusted base: ${key}`); + if (old && allowance.expires > old.expires) + errors.push(`Allowance expiry extended: ${key}`); + } + } + for (const [key, finding] of current) { + if (!allowances.has(key) || finding.value > allowances.get(key).value) { + errors.push(`New or increased violation (${finding.value}): ${key}`); + } + } + return errors; +} + +/** Parse machine-readable output only; a missing report or unexpected schema is an analysis failure. */ +export function parseJson(stdout, analyzer) { + try { + return JSON.parse(stdout); + } catch { + throw new Error(`${analyzer} did not return a complete JSON report`); + } +} diff --git a/tools/quality/lib/git.mjs b/tools/quality/lib/git.mjs new file mode 100644 index 0000000..2aa6dcb --- /dev/null +++ b/tools/quality/lib/git.mjs @@ -0,0 +1,86 @@ +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; + +/** Run a bounded, non-shell child process; preserve failures instead of returning an empty analysis. */ +export function execute(command, args = [], options = {}) { + const result = spawnSync(command, args, { + encoding: 'utf8', + maxBuffer: 128 * 1024 * 1024, + timeout: 600_000, + ...options, + }); + if (result.error || result.signal || result.status === null) { + throw new Error( + `${command} did not complete: ${result.error?.message ?? result.signal}`, + ); + } + return result; +} + +/** Execute read-only Git operations and reject a missing ref or failed comparison. */ +export function git(args) { + const result = execute('git', args); + if (result.status !== 0) + throw new Error(`git ${args[0]} failed: ${result.stderr.trim()}`); + return result.stdout; +} + +/** Resolve one trusted comparison commit; CI explicitly supplies its PR base SHA. */ +export function baseRef( + defaultBranch = 'main', + explicit = process.env.QUALITY_BASE, +) { + if (explicit) + return git(['rev-parse', '--verify', `${explicit}^{commit}`]).trim(); + return git(['merge-base', 'HEAD', `origin/${defaultBranch}`]).trim(); +} + +/** Return tracked paths, including staged additions, with unambiguous NUL delimiters. */ +export function trackedFiles() { + return git(['ls-files', '-z']).split('\0').filter(Boolean); +} + +/** Include committed, staged, unstaged and untracked additions in local changed-file checks. */ +export function changedFiles(base) { + return [ + ...new Set( + [ + ...git([ + 'diff', + '--name-only', + '--diff-filter=ACMR', + '-z', + base, + '--', + ]).split('\0'), + ...git(['ls-files', '--others', '--exclude-standard', '-z']).split( + '\0', + ), + ].filter(Boolean), + ), + ].sort(); +} + +/** Read an optional file at the verified base; missing files mean additions, not failed Git. */ +export function baseFile(base, path) { + const paths = git([ + 'ls-tree', + '-r', + '--name-only', + '-z', + base, + '--', + path, + ]).split('\0'); + return paths.includes(path) ? git(['show', `${base}:${path}`]) : null; +} + +/** Read current bytes only when the path exists; deleted files have no current diagnostics. */ +export function currentFile(path) { + try { + return readFileSync(path, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} diff --git a/tools/quality/lib/guards.mjs b/tools/quality/lib/guards.mjs new file mode 100644 index 0000000..500c8fe --- /dev/null +++ b/tools/quality/lib/guards.mjs @@ -0,0 +1,356 @@ +import ts from 'typescript'; +import picomatch from 'picomatch'; +import { existsSync } from 'node:fs'; +import { baseFile, changedFiles, currentFile, trackedFiles } from './git.mjs'; +import { digest } from './baseline.mjs'; +import { verifyTemplate } from './template.mjs'; + +const sourcePattern = /\.(?:[cm]?[jt]sx?|svelte)$/; + +/** Match repo-relative paths consistently; generated and exception paths stay explicit. */ +export function matches(path, patterns = []) { + return patterns.some((pattern) => picomatch(pattern, { dot: true })(path)); +} + +/** Validate manifest entries before they can remove any source from a mandatory check. */ +export function validateConfig( + config, + today = new Date().toISOString().slice(0, 10), +) { + if ( + config.schemaVersion !== 1 || + !['A', 'B', 'C', 'D'].includes(config.profile) + ) { + throw new Error( + 'quality.config.mjs requires schemaVersion=1 and a concrete profile', + ); + } + if (!Array.isArray(config.sourceRoots) || config.sourceRoots.length === 0) { + throw new Error( + 'sourceRoots must inventory the authored source and tooling', + ); + } + for (const root of config.sourceRoots) { + if (!existsSync(root)) throw new Error(`Missing source root: ${root}`); + } + for (const entry of config.generated ?? []) { + if ( + !entry.producer || + !entry.pattern || + ['**', '**/*', '**/*.ts', '**/*.js'].includes(entry.pattern) + ) { + throw new Error( + 'Generated exclusions require a narrow path and actual producer', + ); + } + } + for (const entry of [ + ...(config.excluded ?? []), + ...(config.comments?.exclude ?? []), + ]) { + if ( + !entry.pattern || + !entry.owner || + !entry.reason || + !entry.expires || + entry.expires < today + ) { + throw new Error(`Unowned or expired source exception: ${entry.pattern}`); + } + } +} + +/** Parse script content with its actual syntax kind; Svelte markup is handled by framework checks. */ +export function parseUnits(path, text) { + if (path.endsWith('.svelte')) { + return [...text.matchAll(/]*)>([\s\S]*?)<\/script\s*>/g)].map( + (match, index) => ({ + suffix: `script${index}`, + ast: ts.createSourceFile( + path, + match[2], + ts.ScriptTarget.Latest, + true, + /lang\s*=\s*["']ts["']/.test(match[1]) + ? ts.ScriptKind.TS + : ts.ScriptKind.JS, + ), + }), + ); + } + const kind = path.endsWith('.tsx') + ? ts.ScriptKind.TSX + : path.endsWith('.jsx') + ? ts.ScriptKind.JSX + : /\.[cm]?ts$/.test(path) + ? ts.ScriptKind.TS + : ts.ScriptKind.JS; + return [ + { + suffix: '', + ast: ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind), + }, + ]; +} + +/** Compare semantic tokens so formatting/quote changes do not create a false method edit. */ +function functionFingerprint(fragment) { + const scanner = ts.createScanner( + ts.ScriptTarget.Latest, + true, + ts.LanguageVariant.JSX, + fragment, + ); + const tokens = []; + for ( + let token = scanner.scan(); + token !== ts.SyntaxKind.EndOfFileToken; + token = scanner.scan() + ) { + const value = [ + ts.SyntaxKind.StringLiteral, + ts.SyntaxKind.NoSubstitutionTemplateLiteral, + ].includes(token) + ? scanner.getTokenValue() + : scanner.getTokenText(); + tokens.push([token, value]); + } + const meaningful = tokens.filter(([kind], index) => { + if (kind !== ts.SyntaxKind.CommaToken) return true; + const next = tokens[index + 1]?.[0]; + if (next === ts.SyntaxKind.CloseBracketToken) { + // Array elisions change length and must never be treated as formatting. + const previous = tokens[index - 1]?.[0]; + return [ + ts.SyntaxKind.CommaToken, + ts.SyntaxKind.OpenBracketToken, + ].includes(previous); + } + return ![ + ts.SyntaxKind.CloseParenToken, + ts.SyntaxKind.CloseBraceToken, + ].includes(next); + }); + return digest(JSON.stringify(meaningful)); +} + +/** Identify callbacks by their lexical owner, not their position among unrelated functions. */ +function lexicalScope(node, ast) { + const owners = []; + for (let current = node.parent; current; current = current.parent) { + if (ts.isFunctionLike(current) || ts.isClassLike(current)) { + const name = current.name ?? current.parent?.name; + if (name) owners.push(name.getText(ast)); + } else if (ts.isCallExpression(current)) { + owners.push(current.expression.getText(ast)); + } + } + return owners.reverse().join('/'); +} + +/** Resolve a function's meaningful enclosing declaration for stable identity and adjacent documentation. */ +function declaration(node) { + let owner = node; + if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + if (ts.isVariableDeclaration(node.parent)) { + owner = node.parent.parent.parent; + } else if ( + ts.isPropertyAssignment(node.parent) || + ts.isPropertyDeclaration(node.parent) + ) { + owner = node.parent; + } + } + return owner; +} + +/** Read adjacent callback docs through casts/assignments, stopping at its own statement or enclosing function. */ +function callbackComments(node, ast) { + const comments = []; + let current = node; + while (current) { + if (current !== node && ts.isFunctionLike(current)) break; + const trivia = ast.text.slice( + current.getFullStart(), + current.getStart(ast), + ); + comments.push(...(trivia.match(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g) ?? [])); + if (ts.isStatement(current)) break; + current = current.parent; + } + return comments; +} + +/** Extract all implemented functions, including private methods, factories and substantive callbacks. */ +export function functionsIn(path, text) { + const functions = []; + for (const { suffix, ast } of parseUnits(path, text)) { + if (ast.parseDiagnostics.length) + throw new Error( + `Cannot parse ${path}: ${ast.parseDiagnostics[0].messageText}`, + ); + const visit = (node) => { + if (ts.isFunctionLike(node) && node.body) { + const owner = declaration(node); + const name = + node.name?.getText(ast) ?? + node.parent.name?.getText(ast) ?? + ''; + const ranges = + ts.getLeadingCommentRanges(ast.text, owner.getFullStart()) ?? []; + const comments = ranges.map((range) => + ast.text.slice(range.pos, range.end), + ); + const parentComments = + node.parent && ts.isCallExpression(node.parent) + ? ( + ts.getLeadingCommentRanges( + ast.text, + node.parent.parent.getFullStart(), + ) ?? [] + ).map((range) => ast.text.slice(range.pos, range.end)) + : []; + functions.push({ + key: `${suffix}:${lexicalScope(node, ast)}:${name}`, + signature: functionFingerprint(node.getText(ast)), + name, + comments: [ + ...comments, + ...parentComments, + ...(name === '' ? callbackComments(node, ast) : []), + ], + line: ast.getLineAndCharacterOfPosition(node.getStart(ast)).line + 1, + callback: name === '', + // An expression-only projection has no independent scenario; its enclosing function is documented. + trivialCallback: + name === '' && + !ts.isBlock(node.body) && + node.body.getText(ast).length < 80 && + !/await|throw|fetch|\bnew\b/.test(node.body.getText(ast)), + }); + } + ts.forEachChild(node, visit); + }; + visit(ast); + } + return functions; +} + +/** Identify suppression directives in comments only, never in strings or generated documentation examples. */ +export function suppressionErrors(path, text, config) { + const errors = []; + for (const { ast } of parseUnits(path, text)) { + const scanner = ts.createScanner( + ts.ScriptTarget.Latest, + false, + ts.LanguageVariant.Standard, + ast.text, + ); + for ( + let token = scanner.scan(); + token !== ts.SyntaxKind.EndOfFileToken; + token = scanner.scan() + ) { + if ( + ![ + ts.SyntaxKind.SingleLineCommentTrivia, + ts.SyntaxKind.MultiLineCommentTrivia, + ].includes(token) + ) + continue; + const comment = scanner.getTokenText(); + if ( + /@ts-(?:ignore|nocheck)\b/.test(comment) || + /eslint-disable(?!-next-line|-line)\b/.test(comment) + ) { + errors.push(`${path}: broad lint/type suppression is forbidden`); + } + if ( + /eslint-disable(?:-next-line|-line)\b/.test(comment) && + !/eslint-disable(?:-next-line|-line)\s+[\w@/-]+(?:\s*,\s*[\w@/-]+)*\s+--\s+.{12,}/.test( + comment, + ) + ) { + errors.push( + `${path}: suppression must name rules and explain the scoped exception`, + ); + } + if ( + /@ts-expect-error\b/.test(comment) && + (!/@ts-expect-error\s*:?\s+.{12,}/.test(comment) || + !matches(path, config.negativeTypeTests ?? [])) + ) { + errors.push( + `${path}: @ts-expect-error requires an explained negative type-test scope`, + ); + } + } + } + return errors; +} + +/** Enforce complete authored-file coverage plus documentation on changed functions and pilot trees. */ +export function runGuards(config, base) { + validateConfig(config); + verifyTemplate(base); + const changed = new Set(changedFiles(base)); + const allFiles = [...new Set([...trackedFiles(), ...changed])]; + const generated = (config.generated ?? []).map((entry) => entry.pattern); + const excluded = (config.excluded ?? []).map((entry) => entry.pattern); + const errors = []; + let checked = 0; + for (const path of allFiles.filter((path) => sourcePattern.test(path))) { + if (path.startsWith('tools/quality/')) continue; // Versioned template has its own tests and documented API. + if (matches(path, [...generated, ...excluded])) continue; + if ( + !config.sourceRoots.some( + (root) => + root === '.' || + path === root || + path.startsWith(`${root.replace(/\/$/, '')}/`), + ) + ) { + errors.push(`${path}: authored file has no declared source root`); + continue; + } + const text = currentFile(path); + if (text === null) continue; + checked++; + const full = matches(path, config.comments?.full); + if (!changed.has(path) && !full) continue; + errors.push(...suppressionErrors(path, text, config)); + if ( + matches( + path, + (config.comments?.exclude ?? []).map((entry) => entry.pattern), + ) + ) + continue; + const before = baseFile(base, path); + const previous = new Map(); + for (const fn of before === null ? [] : functionsIn(path, before)) { + const identity = `${fn.key}:${fn.signature}`; + previous.set(identity, (previous.get(identity) ?? 0) + 1); + } + for (const fn of functionsIn(path, text)) { + const identity = `${fn.key}:${fn.signature}`; + const remaining = previous.get(identity) ?? 0; + if (remaining > 0) previous.set(identity, remaining - 1); + if ((!full && remaining > 0) || fn.trivialCallback) continue; + const docs = fn.comments + .filter((comment) => fn.callback || comment.startsWith('/**')) + .join('\n'); + const words = docs + .replace(/\{@[^}]+\}|\{[^}]+\}|\*|\//g, ' ') + .trim() + .split(/\s+/); + if (words.length < 3 || docs.length < 18) + errors.push( + `${path}:${fn.line}: document ${fn.name}'s purpose and contract`, + ); + } + } + if (checked === 0 && config.profile !== 'D') + errors.push('No authored JS/TS/Svelte files were checked'); + return { checked, errors }; +} diff --git a/tools/quality/lib/historical.mjs b/tools/quality/lib/historical.mjs new file mode 100644 index 0000000..7ed6be8 --- /dev/null +++ b/tools/quality/lib/historical.mjs @@ -0,0 +1,139 @@ +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { execute, git } from './git.mjs'; +import picomatch from 'picomatch'; + +/** Omit only new entry/project targets that cannot exist in the historical source being measured. */ +function historicalPatterns(value, prefix, currentPaths, basePaths) { + const patterns = typeof value === 'string' ? [value] : value; + if (!Array.isArray(patterns)) return value; + return patterns.filter((pattern) => { + if (typeof pattern !== 'string' || pattern.startsWith('!')) return true; + const match = picomatch( + join(prefix, pattern.replace(/!$/, '')).replaceAll('\\', '/'), + { dot: true }, + ); + return ( + basePaths.some((path) => match(path)) || + !currentPaths.some((path) => match(path)) + ); + }); +} + +/** Keep current Knip policy but remove newly introduced tooling-only roots from historical coverage. */ +function prepareHistoricalKnip(path, currentPaths, basePaths) { + if (!path.endsWith('.json') || !existsSync(path)) return; + const config = JSON.parse(readFileSync(path, 'utf8')); + // The sealed vendor snapshot is new during initial adoption. Its current-only + // import roots are removed below, so this exact exemption would otherwise be stale. + if ( + !basePaths.some((path) => path.startsWith('tools/quality/')) && + Array.isArray(config.ignore) + ) { + config.ignore = config.ignore.filter( + (pattern) => pattern !== 'tools/quality/**', + ); + } + const scopes = [['.', config], ...Object.entries(config.workspaces ?? {})]; + for (const [prefix, scope] of scopes) { + if (!scope || typeof scope !== 'object') continue; + for (const key of ['entry', 'project']) { + if (scope[key]) + scope[key] = historicalPatterns( + scope[key], + prefix, + currentPaths, + basePaths, + ); + } + } + writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`); +} + +/** Re-run the same analyzer against trusted source with the newly adopted analysis configuration. */ +export function historicalAnalysis(base, config, templateDirectory, analyze) { + const current = process.cwd(); + const snapshot = mkdtempSync(join(tmpdir(), '1msg-quality-base-')); + const archiveDirectory = mkdtempSync(join(tmpdir(), '1msg-quality-archive-')); + try { + const archivePath = join(archiveDirectory, 'source.tar'); + const archive = execute('git', [ + 'archive', + '--format=tar', + '--output', + archivePath, + base, + ]); + if (archive.status !== 0) + throw new Error(`Cannot read trusted source: ${archive.stderr}`); + const extract = execute('tar', ['-xf', archivePath, '-C', snapshot]); + if (extract.status !== 0) + throw new Error(`Cannot unpack trusted source: ${extract.stderr}`); + const paths = git(['ls-files', '-co', '--exclude-standard', '-z']) + .split('\0') + .filter(Boolean); + const basePaths = git(['ls-tree', '-r', '--name-only', '-z', base]) + .split('\0') + .filter(Boolean); + // Some legacy multi-package repos adopt their first root manifest with the quality tooling. + // Findings on this new manifest still cannot become debt: initialEvidence requires a base file. + if ( + !existsSync(join(snapshot, 'package.json')) && + existsSync('package.json') + ) { + cpSync('package.json', join(snapshot, 'package.json')); + } + const configuration = paths.filter((path) => + /(?:^|\/)(?:eslint(?:\.[\w-]+)?\.config\.[cm]?js|dependency-cruiser(?:\.[\w-]+)?\.config\.[cm]?js|tsconfig(?:\.[\w-]+)?\.json|quality\.config\.mjs)$/.test( + path, + ), + ); + configuration.push(config.knipConfig ?? 'knip.json'); + for (const path of new Set(configuration)) { + if (!existsSync(path)) continue; + mkdirSync(dirname(join(snapshot, path)), { recursive: true }); + cpSync(path, join(snapshot, path)); + } + prepareHistoricalKnip( + join(snapshot, config.knipConfig ?? 'knip.json'), + paths, + basePaths, + ); + // Dependencies are analyzer inputs; historical application source always comes from Git above. + const packages = new Set([ + '.', + ...paths.filter((path) => path.endsWith('/package.json')).map(dirname), + ]); + for (const path of packages) { + const modules = resolve(current, path, 'node_modules'); + const destination = join(snapshot, path, 'node_modules'); + if (!existsSync(modules) || existsSync(destination)) continue; + mkdirSync(dirname(destination), { recursive: true }); + symlinkSync(modules, destination, 'dir'); + } + const destination = join(snapshot, 'tools/quality'); + rmSync(destination, { recursive: true, force: true }); + mkdirSync(dirname(destination), { recursive: true }); + symlinkSync(templateDirectory, destination, 'dir'); + process.chdir(snapshot); + const historicalConfig = { + ...config, + duplicatePaths: config.duplicatePaths?.filter((path) => existsSync(path)), + }; + return analyze(historicalConfig); + } finally { + process.chdir(current); + rmSync(snapshot, { recursive: true, force: true }); + rmSync(archiveDirectory, { recursive: true, force: true }); + } +} diff --git a/tools/quality/lib/knip-reporter.mjs b/tools/quality/lib/knip-reporter.mjs new file mode 100644 index 0000000..87999ef --- /dev/null +++ b/tools/quality/lib/knip-reporter.mjs @@ -0,0 +1,42 @@ +import jsonReporter from '../node_modules/knip/dist/reporters/json.js'; + +/** Reject incomplete Knip graphs before emitting the pinned version's standard JSON report. */ +export default async function qualityReporter(options) { + if (options.hasConfigLoadErrors || options.counters.processed === 0) { + throw new Error( + 'Knip failed to load configuration or analyzed an empty graph', + ); + } + for (const required of [ + 'files', + 'exports', + 'dependencies', + 'unlisted', + 'unresolved', + ]) { + if (!options.report[required]) + throw new Error(`Mandatory Knip issue type disabled: ${required}`); + } + const incomplete = options.configurationHints.filter((hint) => + [ + 'entry-empty', + 'project-empty', + 'project-extension-unregistered', + 'project-extension-excluded', + 'workspace-unconfigured', + 'top-level-unconfigured', + 'ignore', + 'ignoreFiles', + 'ignoreBinaries', + 'ignoreDependencies', + 'ignoreUnresolved', + 'ignoreWorkspaces', + ].includes(hint.type), + ); + if (incomplete.length) { + throw new Error( + `Knip coverage/stale exclusion failure: ${incomplete.map((hint) => `${hint.type}:${hint.identifier}`).join(', ')}`, + ); + } + await jsonReporter(options); +} diff --git a/tools/quality/lib/reports.mjs b/tools/quality/lib/reports.mjs new file mode 100644 index 0000000..19c9a8e --- /dev/null +++ b/tools/quality/lib/reports.mjs @@ -0,0 +1,265 @@ +import { relative, isAbsolute } from 'node:path'; +import { existsSync } from 'node:fs'; +import ts from 'typescript'; +import { currentFile } from './git.mjs'; +import { digest, record } from './baseline.mjs'; + +/** Convert report paths to portable repository paths; external paths are not silently accepted. */ +export function repoPath(path) { + if (typeof path !== 'string' || !path) + throw new Error('Analyzer returned a missing file path'); + const normalized = (isAbsolute(path) ? relative(process.cwd(), path) : path) + .replaceAll('\\', '/') + .replace(/^\.\//, ''); + if (normalized === '..' || normalized.startsWith('../')) + throw new Error(`Analyzer escaped repository: ${path}`); + return normalized; +} + +/** Preserve all enabled Knip issue categories and distinguish separate symbols in the same file. */ +export function knipRecords(report) { + if (!report || !Array.isArray(report.issues)) + throw new Error('Invalid Knip JSON schema'); + const results = []; + for (const row of report.issues) { + const path = repoPath(row.file); + for (const [type, issues] of Object.entries(row)) { + if (['file', 'owners'].includes(type)) continue; + if (!Array.isArray(issues)) + throw new Error(`Invalid Knip category: ${type}`); + const grouped = new Map(); + for (const issue of issues) { + const symbols = (Array.isArray(issue) ? issue : [issue]).map( + (item) => item.name ?? item.symbol, + ); + if (symbols.some((symbol) => typeof symbol !== 'string')) + throw new Error(`Invalid Knip symbol: ${path}/${type}`); + const symbol = symbols.sort().join(','); + grouped.set(symbol, (grouped.get(symbol) ?? 0) + 1); + } + for (const [symbol, count] of grouped) + results.push( + record(`${path}|${type}|${symbol}`, count, { + path, + rule: type, + symbol, + }), + ); + } + } + return results.sort((a, b) => a.key.localeCompare(b.key)); +} + +/** Normalize token evidence without stripping string values or treating comments as meaningful duplicates. */ +export function tokenFingerprint(fragment) { + const scanner = ts.createScanner( + ts.ScriptTarget.Latest, + true, + ts.LanguageVariant.JSX, + fragment, + ); + const tokens = []; + for ( + let token = scanner.scan(); + token !== ts.SyntaxKind.EndOfFileToken; + token = scanner.scan() + ) { + tokens.push([token, scanner.getTokenText()]); + } + return digest(JSON.stringify(tokens)); +} + +/** Parse actual dependency-cruiser output; only pre-existing runtime cycles may be tracked as debt. */ +export function architectureRecords(report) { + if ( + !Array.isArray(report?.modules) || + !report.modules.length || + !report.summary?.totalCruised || + !Array.isArray(report.summary.violations) + ) + throw new Error('dependency-cruiser returned invalid/empty coverage'); + const cycles = new Map(); + for (const violation of report.summary.violations) { + if ( + violation.type !== 'cycle' || + violation.rule?.name !== 'no-runtime-cycles' || + !Array.isArray(violation.cycle) || + !violation.cycle.length + ) { + throw new Error( + `Non-baselinable architecture violation: ${violation.rule?.name}: ${violation.from} -> ${violation.to}`, + ); + } + const members = violation.cycle.map((member) => repoPath(member.name)); + const rotations = members.map((_, index) => + [...members.slice(index), ...members.slice(0, index)].join(' -> '), + ); + const key = `no-runtime-cycles|${rotations.sort()[0]}`; + cycles.set( + key, + record(key, 1, { + rule: 'no-runtime-cycles', + paths: [...new Set(members)].sort(), + }), + ); + } + return [...cycles.values()].sort((a, b) => a.key.localeCompare(b.key)); +} + +/** Resolve jscpd's embedded Svelte fragments to an existing source while retaining their language identity. */ +function duplicateSource(path) { + const participant = repoPath(path); + const embedded = + /^(.*\.svelte):(html|css|scss|sass|less|stylus|javascript|typescript|jsx|tsx)$/.exec( + participant, + ); + if (!embedded) { + if (/\.svelte:/.test(participant)) + throw new Error(`Unknown Svelte clone fragment: ${participant}`); + return { path: participant, participant, embedded: false }; + } + const source = embedded[1]; + if (!existsSync(source)) + throw new Error(`Missing Svelte clone source: ${source}`); + return { path: source, participant, embedded: true }; +} + +/** Identify clone groups by normalized code and participants, so unrelated unique code cannot dilute violations. */ +export function duplicateRecords(report) { + if ( + !report || + !Array.isArray(report.duplicates) || + !report.statistics?.total?.sources + ) { + throw new Error('jscpd report is missing or has empty source coverage'); + } + const grouped = new Map(); + for (const clone of report.duplicates) { + const first = duplicateSource(clone.firstFile?.name); + const second = duplicateSource(clone.secondFile?.name); + let fragment = clone.fragment; + if (typeof fragment !== 'string') { + if (first.embedded || second.embedded) + throw new Error('jscpd omitted embedded fragment evidence'); + const source = currentFile(first.path); + if ( + source === null || + !Number.isInteger(clone.firstFile.start) || + !Number.isInteger(clone.firstFile.end) + ) { + throw new Error('jscpd omitted clone evidence'); + } + fragment = source + .split(/\r?\n/) + .slice(clone.firstFile.start - 1, clone.firstFile.end) + .join('\n'); + } + const fingerprint = `${clone.format}|${tokenFingerprint(fragment)}`; + const group = grouped.get(fingerprint) ?? { + paths: new Set(), + participants: new Set(), + pairs: 0, + tokens: 0, + }; + group.paths.add(first.path); + group.paths.add(second.path); + group.participants.add(first.participant); + group.participants.add(second.participant); + group.pairs++; + group.tokens = Math.max(group.tokens, clone.tokens ?? 0); + grouped.set(fingerprint, group); + } + return [...grouped] + .map(([fingerprint, group]) => { + const paths = [...group.paths].sort(); + const participants = [...group.participants].sort(); + return record(`${fingerprint}|${participants.join('|')}`, group.pairs, { + paths, + ...(participants.some((path) => /\.svelte:/.test(path)) + ? { participants } + : {}), + tokens: group.tokens, + }); + }) + .sort((a, b) => a.key.localeCompare(b.key)); +} + +/** Locate the enclosing declaration for an ESLint diagnostic without using unstable line numbers as identity. */ +function scopeAt(source, line, column) { + const offset = source.getPositionOfLineAndCharacter( + Math.max(0, line - 1), + Math.max(0, column - 1), + ); + let scope = ''; + const visit = (node) => { + if (node.pos <= offset && node.end >= offset) { + if (ts.isFunctionLike(node)) + scope = + node.name?.getText(source) ?? + node.parent.name?.getText(source) ?? + ''; + ts.forEachChild(node, visit); + } + }; + visit(source); + return scope; +} + +/** Only complexity diagnostics may use a debt baseline; parser, typed and correctness errors remain fatal. */ +export function eslintRecords(report) { + if (!Array.isArray(report) || report.length === 0) + throw new Error('ESLint returned empty source coverage'); + const allowed = new Set([ + 'complexity', + 'max-depth', + 'max-params', + 'max-lines', + 'max-lines-per-function', + 'sonarjs/cognitive-complexity', + ]); + const results = []; + for (const file of report) { + const path = repoPath(file.filePath); + if (!Array.isArray(file.messages)) throw new Error('Invalid ESLint report'); + const source = ts.createSourceFile( + path, + currentFile(path) ?? '', + ts.ScriptTarget.Latest, + true, + ); + const occurrences = new Map(); + for (const message of file.messages) { + if (message.fatal || !allowed.has(message.ruleId)) { + throw new Error( + `${path}: non-baselinable ${message.ruleId ?? 'parser'}: ${message.message}`, + ); + } + const metricPatterns = { + complexity: /complexity of (\d+)/i, + 'max-depth': /(?:nested too deeply|maximum allowed).*?\((\d+)\)/i, + 'max-params': /too many parameters \((\d+)\)/i, + 'max-lines': /too many lines \((\d+)\)/i, + 'max-lines-per-function': /too many lines \((\d+)\)/i, + 'sonarjs/cognitive-complexity': /complexity from (\d+)/i, + }; + const values = metricPatterns[message.ruleId].exec(message.message); + if (!values) + throw new Error(`No metric in ESLint diagnostic: ${message.message}`); + const scope = + message.ruleId === 'max-lines' + ? '' + : scopeAt(source, message.line ?? 1, message.column ?? 1); + const identity = `${path}|${message.ruleId}|${scope}`; + const occurrence = occurrences.get(identity) ?? 0; + occurrences.set(identity, occurrence + 1); + results.push( + record(`${identity}|${occurrence}`, Number(values[1]), { + path, + rule: message.ruleId, + scope, + }), + ); + } + } + return results.sort((a, b) => a.key.localeCompare(b.key)); +} diff --git a/tools/quality/lib/template.mjs b/tools/quality/lib/template.mjs new file mode 100644 index 0000000..0a31562 --- /dev/null +++ b/tools/quality/lib/template.mjs @@ -0,0 +1,56 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { baseFile } from './git.mjs'; +import { digest } from './baseline.mjs'; + +/** Inventory all vendored files except installed dependencies; unexpected symlinks cannot hide source. */ +export function templateFiles(root, relative = '') { + return readdirSync(join(root, relative), { withFileTypes: true }) + .flatMap((entry) => { + const path = join(relative, entry.name).replaceAll('\\', '/'); + if (path === 'node_modules' || path === 'template-manifest.json') + return []; + if (entry.isSymbolicLink()) + throw new Error(`Untracked template symlink: ${path}`); + return entry.isDirectory() ? templateFiles(root, path) : [path]; + }) + .sort(); +} + +/** Keep the vendored exemption exact and require a new template version for changed preset contents. */ +export function verifyTemplate(base) { + const root = 'tools/quality'; + if (!existsSync(root)) return; // Standalone CLI fixtures exercise the runner outside a vendored checkout. + const manifestPath = `${root}/template-manifest.json`; + if (!existsSync(manifestPath)) + throw new Error( + 'Vendored quality template is missing its versioned inventory', + ); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + const packageInfo = JSON.parse(readFileSync(`${root}/package.json`, 'utf8')); + if (manifest.schemaVersion !== 1 || manifest.version !== packageInfo.version) + throw new Error('Template version mismatch'); + const files = templateFiles(root); + if ( + JSON.stringify(files) !== JSON.stringify(Object.keys(manifest.files).sort()) + ) + throw new Error('Quality template has undeclared/missing files'); + for (const path of files) { + if ( + digest(readFileSync(`${root}/${path}`, 'utf8')) !== manifest.files[path] + ) + throw new Error(`Quality template integrity mismatch: ${path}`); + } + const previousText = baseFile(base, manifestPath); + if (previousText) { + const previous = JSON.parse(previousText); + if ( + previous.version === manifest.version && + JSON.stringify(previous.files) !== JSON.stringify(manifest.files) + ) { + throw new Error( + 'Quality template changed without an explicit version update', + ); + } + } +} diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json new file mode 100644 index 0000000..5b96b4b --- /dev/null +++ b/tools/quality/package-lock.json @@ -0,0 +1,6852 @@ +{ + "name": "@1msg/quality-template", + "version": "1.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@1msg/quality-template", + "version": "1.0.2", + "dependencies": { + "@eslint/js": "9.39.5", + "dependency-cruiser": "18.6.0", + "eslint": "9.39.5", + "eslint-import-resolver-typescript": "4.4.5", + "eslint-plugin-import": "2.32.0", + "eslint-plugin-n": "18.4.1", + "eslint-plugin-perfectionist": "5.12.1", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-sonarjs": "3.0.7", + "eslint-plugin-unicorn": "62.0.0", + "globals": "17.13.0", + "husky": "9.1.7", + "jscpd": "5.4.1", + "knip": "6.41.0", + "lint-staged": "16.2.7", + "picomatch": "4.0.7", + "prettier": "3.9.10", + "typescript": "5.9.3", + "typescript-eslint": "8.71.1" + }, + "engines": { + "node": "24.x" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.153.0.tgz", + "integrity": "sha512-BcWu25dMwMjHpaDCEidmpAi7rGg1qZoihJeUUH9bD55pVZ0z4Wa5CQgqSTo0R41H81oozisQKW3WqvAN2m77IQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.153.0.tgz", + "integrity": "sha512-9l5rdp2DhRqVriBHsNpu5cEf+hizZ4B58rt93HzyQEBUgZiC6qe2oFpaAAnXGCxHGnQExI1s7S2G0QhFacUZ6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.153.0.tgz", + "integrity": "sha512-EcpsyRXHhENDpekpG4QNaSZj2F/BAz4Gh4o89zZwNss86R8KyRSQ6sXbAj74Xyeb4z3j/yBZZ6joQZhraIWyfw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.153.0.tgz", + "integrity": "sha512-hlj80HUQy07/2lYp6rmxH+3u+H2ik3rjLR52CUdPApxmZCNxASDUf/+ykTzAHS/fehftmVMATKvPaWmuLHuasg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.153.0.tgz", + "integrity": "sha512-fWRcv2pMSh1qtez46MOu8vuUJ5fiaQZPHSphbWulbL7SHBMKfRgZl2PzjXZYQJbq7NEXyXbgIuIuUm+/Rmkv8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.153.0.tgz", + "integrity": "sha512-paZeUK1GXShl/bOJQET1/8AQxayFeuN4SIWAkgTRxE7ZQwv9uYSB2n0648smygP22N7jcI3zxAm0Z+ALNiIKxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.153.0.tgz", + "integrity": "sha512-qFPp5NwMf4K1C0O+DCVGmcbOaYyYAq98Rvv9p4VSfb777iUwlJCqMUxf6QG9N5f5ls4TEX6LitOZHi+6GL6rYQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.153.0.tgz", + "integrity": "sha512-oufEhZCX7saeo27Logra8cvBR1jQ/yw2zUI2hHmWw3qA/XYqEW31FOvLQBv7DWN32NjVdlcaP11WBBZTJo4k9g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.153.0.tgz", + "integrity": "sha512-zXYzuAqng85h6+LI3iRm0zeMLX1/K2z41ZAMWxVRgt0D10VvZUaV5q+TEB46KDhoKBpdexoTLC6QoB/EiP2oqQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.153.0.tgz", + "integrity": "sha512-J7+NLwLEzdkKM9cKyPHH/AjXXstWOs98KYgLrPPnO4N9MO8EZRBfPNnBcS3S67uMOBkRpUmCl2FhRnWdQ5CbBA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.153.0.tgz", + "integrity": "sha512-sD0K11GQTLWylln54nPMMTtgmBbQrR5It/vMpZpy/fogiw8O2AvIE+9LuWUXddrWk9zvFIHGRcSgFn4IHOfoYg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.153.0.tgz", + "integrity": "sha512-7L66uql2BhvSz51LwfExjOdn6et/ydTiQxl23VApdKzJhTc3TczA1Qt6VR/UcvlQrL2ZbytIJb447vh7p5wmUw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.153.0.tgz", + "integrity": "sha512-hXV3T7mNyrPR19rbPYkglkQJtoM/UNo/xwbZeeqdAxEg8PXgb4LXXuxPpGgI4bzRRCWwLUxdDSS58xIyC2u1TA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.153.0.tgz", + "integrity": "sha512-Da8Vqs1xdtoDYtEjmozD7wGgJeLBQkMbR0WUBECOSxodyX5DXy8ySlvJWbukyO2Q0+TM9EK+4LT6tbq0jCg4hQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.153.0.tgz", + "integrity": "sha512-zGAPxNTIIw6X5WyTRscIFE/oDjpdNtrrl32ing6NOXdGjzRucadnxBilexLdeYQ9YxixF93GujpXyQ0jw7ddSg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.153.0.tgz", + "integrity": "sha512-GoHzpqP5XyPIp9DHokoEdCp+K4UPl/h1neFj1uXnUB3pJYyOQ7NOhFVOttNUgCGcj2eywAvu5cRivTOSq4S10g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.153.0.tgz", + "integrity": "sha512-xGcy12tO9k7zFbeyQzEuilfWfQpyRq23/fLPfN3wjSWuWv58k46fQT1hsO1KFNykZuVGW9xSJE/z592TmAoSDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.153.0.tgz", + "integrity": "sha512-o1ykUjWncvXX6N5a4KebgLjtD8zRui+f1llMhSGoSC9oP2KOqapRpdwmZ3NcNG0axFouwVKmhGBjd+IhA8BksA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.153.0.tgz", + "integrity": "sha512-gxQz7G96Up136fd+2zWrP+o6FYGs9NO4Dyv55IoeJFJUDwLFHcLYY11hhA5wQKs0HoM57Q8esQ3nEVhnCdsjAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz", + "integrity": "sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/type-utils": "8.71.1", + "@typescript-eslint/utils": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.1.tgz", + "integrity": "sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.1.tgz", + "integrity": "sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.1", + "@typescript-eslint/types": "^8.71.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.1.tgz", + "integrity": "sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1" + }, + "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.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.1.tgz", + "integrity": "sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.1.tgz", + "integrity": "sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/utils": "8.71.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.1.tgz", + "integrity": "sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==", + "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.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.1.tgz", + "integrity": "sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.1", + "@typescript-eslint/tsconfig-utils": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.1.tgz", + "integrity": "sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.1.tgz", + "integrity": "sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "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/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==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dependency-cruiser": { + "version": "18.6.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-18.6.0.tgz", + "integrity": "sha512-It7NW5AmCDBCLMq7I91Nfu8VRKzrIE6S+M/FMQzALDceJ0B1PWRHo77q9YJg/HwyBBEWHVtOuTptIQki+xecOw==", + "license": "MIT", + "dependencies": { + "acorn": "8.19.0", + "acorn-jsx": "5.3.2", + "acorn-jsx-walk": "2.0.0", + "acorn-loose": "8.5.2", + "acorn-walk": "8.3.5", + "commander": "15.0.0", + "enhanced-resolve": "5.26.0", + "ignore": "7.0.12", + "interpret": "3.1.1", + "is-installed-globally": "1.0.1", + "json5": "2.2.3", + "picomatch": "4.0.7", + "prompts": "2.4.2", + "rechoir": "0.8.0", + "safe-regex": "2.1.1", + "semver": "7.8.5", + "tsconfig-paths-webpack-plugin": "4.2.0", + "watskeburt": "6.0.0" + }, + "bin": { + "depcruise": "bin/dependency-cruiser.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruiser.mjs", + "dependency-cruiser": "bin/dependency-cruiser.mjs" + }, + "engines": { + "node": "^22||^24||>=26" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.452", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz", + "integrity": "sha512-cVvk1YBSj6571VmV6hdJTqj94Y79xSu8cj6Y9t6ae4Y4431z8/7EJFvu3rVp90xhfuv9BpPKUmaLfTo4+FOGBg==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz", + "integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==", + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-n": { + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.4.1.tgz", + "integrity": "sha512-2sBReWOvCbzgkPri9M5SFmD5EbIwZLa1xz6B5RZsrI19WSFKpyx1sOMr+pdk8puCiXuEgCbErEHy1PuaUrc/4w==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.5.0", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "globrex": "^0.1.2", + "ignore": "^5.3.2", + "js-yaml": "^5.4.1", + "semver": "^7.6.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.57.1", + "ts-declaration-location": "^1.0.6", + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "ts-declaration-location": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-plugin-n/node_modules/js-yaml": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz", + "integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/eslint-plugin-perfectionist": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.12.1.tgz", + "integrity": "sha512-krdSE7Dz+TCtcVr2+rfNhlOIFFBJcUdoRvjaN7JQnsaVnxJw+BZh9wMkY3MSLCJat+yW+UPTQLKS82Epc7MGZA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.70.0", + "@typescript-eslint/utils": "^8.70.0", + "natural-orderby": "^5.0.0" + }, + "engines": { + "node": "^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-sonarjs": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-3.0.7.tgz", + "integrity": "sha512-62jB20krIPvcwBLAyG3VVKa2ce2j2lL1yCb8Y0ylMRR/dLvCCTiQx8gQbXb+G81k1alPZ2/I3muZinqWQdBbzw==", + "license": "LGPL-3.0-only", + "dependencies": { + "@eslint-community/regexpp": "4.12.2", + "builtin-modules": "3.3.0", + "bytes": "3.1.2", + "functional-red-black-tree": "1.0.1", + "jsx-ast-utils-x": "0.1.0", + "lodash.merge": "4.6.2", + "minimatch": "10.1.2", + "scslre": "0.3.0", + "semver": "7.7.4", + "typescript": ">=5" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "62.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-62.0.0.tgz", + "integrity": "sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "@eslint-community/eslint-utils": "^4.9.0", + "@eslint/plugin-kit": "^0.4.0", + "change-case": "^5.4.4", + "ci-info": "^4.3.1", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.46.0", + "esquery": "^1.6.0", + "find-up-simple": "^1.0.1", + "globals": "^16.4.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.13.0", + "semver": "^7.7.3", + "strip-indent": "^4.1.1" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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==", + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-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==", + "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==", + "license": "MIT" + }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/formatly": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.11.4.tgz", + "integrity": "sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA==", + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0", + "package-manager-detector": "^1.8.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.14.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-builtin-module/node_modules/builtin-modules": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.4.0.tgz", + "integrity": "sha512-JCWSCdun+4Ovd9BhM/Vtlmwb7oD6Wl4YiPRXXwhAuwlPhW1un/MKgXn7GZoFm53ginnoNzus69V8JWx0K393jg==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.1.tgz", + "integrity": "sha512-li88H4QGRoNMX27jEmY8nR515oJVspAgC/WFMlhHfFV8RJ4QyGpHhXnwAe9gH5RIgKmdYRUmrOhQobcgEjIdSw==", + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jscpd": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.4.1.tgz", + "integrity": "sha512-afUt+LSQMmzSPYrWZlNGTwj8X4tmwVJbMNmr92qxPf4uUF/naLwG7SQyMPEZtrHqGgzGxmAzNjiwlVdjls2MyQ==", + "license": "MIT", + "bin": { + "jscpd": "run-jscpd.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://opencollective.com/jscpd" + }, + "optionalDependencies": { + "jscpd-darwin-arm64": "5.4.1", + "jscpd-darwin-x64": "5.4.1", + "jscpd-linux-arm64-gnu": "5.4.1", + "jscpd-linux-arm64-musl": "5.4.1", + "jscpd-linux-x64-gnu": "5.4.1", + "jscpd-linux-x64-musl": "5.4.1", + "jscpd-windows-arm64-msvc": "5.4.1", + "jscpd-windows-x64-msvc": "5.4.1" + } + }, + "node_modules/jscpd-darwin-arm64": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.4.1.tgz", + "integrity": "sha512-LXHVeH60C9BsozcVIMPmXWvj97KqFKbCJYmYG4hDwfwIwS1kz+uF7yiObckP6h2xo12pD88nI4XjWTB5frjZUw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/jscpd-darwin-x64": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.4.1.tgz", + "integrity": "sha512-Gqck4tcMZjv4IvYHU2Vcikb885zTADo5T/YfEqeu1i5W5tfFpLdViiTYrh5wbrW+uxBqC33gHpG1hy0xbGYknQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/jscpd-linux-arm64-gnu": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.4.1.tgz", + "integrity": "sha512-XUYZJZVxSY7JIBEoyOOMTgvJwmymkns4wG55tu6I/vRKXnS5QtaTESQfRLt6xj1+62aflAvr01FAhPAy+du6tA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-arm64-musl": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-musl/-/jscpd-linux-arm64-musl-5.4.1.tgz", + "integrity": "sha512-h2sH8Bk++yBxWqq2QAy4fvPzXkw1vecDSyOmzLwPfnRQoQ2Q/EWxCNOrgJSb+SNMLWoHLLzfqx8m9wt2GG6a8w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-x64-gnu": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.4.1.tgz", + "integrity": "sha512-UiC4YoGAVVLy6hPVz8A6cndo7W5br4FAO2hADnFeinhBPnXPu0QFh1q/s/8B3ldeQHQkpYd/l5cjqNBMrbm78w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-x64-musl": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.4.1.tgz", + "integrity": "sha512-toAafl5roqfFotPrDJNMY9noNVZ9dReBqcO/nT5pJ8+7aq+opbscaLUcV4+8jxTq5btUD6CiO72+Fu+TyUZv3Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-windows-arm64-msvc": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-windows-arm64-msvc/-/jscpd-windows-arm64-msvc-5.4.1.tgz", + "integrity": "sha512-JLMga/NIwv/OPurPsXA2/d5OdWfdwrsBk2D4NhyKnrV/uNjo1BIi2zXztzDyTQVKMK/TEK6GPgLD143GG1fZ0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/jscpd-windows-x64-msvc": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.4.1.tgz", + "integrity": "sha512-sN57Y9YP3ijrqc1IsWDceOFHUwbukg9/mU2NSDHTryKKc5CUvWz+o9GId8uKTiJIhkKfR/Ce5GKkCepTwB6kaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jsx-ast-utils-x": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils-x/-/jsx-ast-utils-x-0.1.0.tgz", + "integrity": "sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/knip": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.41.0.tgz", + "integrity": "sha512-DrhQlIuV6xScd5Q+WTF1znd1w41QzxFGCtAHXCajsH/ndGMotHoNYbdXbuFtzW3mIOmniHtZ7zCtvnQkUmJtQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "formatly": "^0.11.4", + "get-tsconfig": "4.14.3", + "jiti": "^2.7.0", + "oxc-parser": "^0.153.0", + "oxc-resolver": "11.24.2", + "picomatch": "^4.0.7", + "smol-toml": "^1.9.0", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^5.0.0", + "yaml": "^2.9.1", + "zod": "^4.6.5" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "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/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==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "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==", + "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/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.1.0.tgz", + "integrity": "sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==", + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/natural-orderby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz", + "integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oxc-parser": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.153.0.tgz", + "integrity": "sha512-cEnpyyV1OJRXv++SFNw0z8i+2o/cr88rVLaOYJafELDoHM5yHvCAbwGRLvxuany6SNLcbblSMUyZaGzH+quSYg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.153.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.153.0", + "@oxc-parser/binding-android-arm64": "0.153.0", + "@oxc-parser/binding-darwin-arm64": "0.153.0", + "@oxc-parser/binding-darwin-x64": "0.153.0", + "@oxc-parser/binding-freebsd-x64": "0.153.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.153.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.153.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.153.0", + "@oxc-parser/binding-linux-arm64-musl": "0.153.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.153.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.153.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.153.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.153.0", + "@oxc-parser/binding-linux-x64-gnu": "0.153.0", + "@oxc-parser/binding-linux-x64-musl": "0.153.0", + "@oxc-parser/binding-openharmony-arm64": "0.153.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.153.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.153.0", + "@oxc-parser/binding-win32-x64-msvc": "0.153.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "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_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.1.tgz", + "integrity": "sha512-e0F9AOF1JMrCfBsyJOwU9lNvQ0WtXTq0j/4jk0BQ5JSI9VAybPXmDpPRw/2FQ3e5d3ZFN1mLh7jW99m/jjaptw==", + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rechoir/node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regjsparser": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.4.tgz", + "integrity": "sha512-EWV80YgFaVgFrjGCAsV7oybp4GOxbls5RzBg8OB0nKEVXsBJR1JlV8Di1JhCROg3MzTfe1RI8yitIm1eOs68iA==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "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==", + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "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==", + "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/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smol-toml": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "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/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "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==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.1.tgz", + "integrity": "sha512-oiNVPC3/NbV1FpaHU07l8O6ynqG9v4PAiW2WvTrAmZdR0f2HI/gE668ElMLCK7Xbsu5k2fK5C4fXNiq05j7/nQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.1", + "@typescript-eslint/parser": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/utils": "8.71.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbash": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-5.0.0.tgz", + "integrity": "sha512-RYNm/ISFtflWpIcXTPHHLJZ0mqzInav+L9ke9PcL+hHsSf9+Fd/syPb614Mlqs/O3AYgjnvxDOFl0a+zVqi1/w==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/watskeburt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-6.0.0.tgz", + "integrity": "sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==", + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^22.13||^24||>=26" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "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", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/tools/quality/package.json b/tools/quality/package.json new file mode 100644 index 0000000..62d20c2 --- /dev/null +++ b/tools/quality/package.json @@ -0,0 +1,34 @@ +{ + "name": "@1msg/quality-template", + "version": "1.0.2", + "private": true, + "type": "module", + "engines": { + "node": "24.x" + }, + "scripts": { + "test": "node --test test/*.test.mjs" + }, + "dependencies": { + "@eslint/js": "9.39.5", + "dependency-cruiser": "18.6.0", + "eslint": "9.39.5", + "eslint-import-resolver-typescript": "4.4.5", + "eslint-plugin-import": "2.32.0", + "eslint-plugin-n": "18.4.1", + "eslint-plugin-perfectionist": "5.12.1", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-sonarjs": "3.0.7", + "eslint-plugin-unicorn": "62.0.0", + "globals": "17.13.0", + "husky": "9.1.7", + "jscpd": "5.4.1", + "knip": "6.41.0", + "lint-staged": "16.2.7", + "picomatch": "4.0.7", + "prettier": "3.9.10", + "typescript": "5.9.3", + "typescript-eslint": "8.71.1" + } +} diff --git a/tools/quality/prettier.config.mjs b/tools/quality/prettier.config.mjs new file mode 100644 index 0000000..9d0c3e3 --- /dev/null +++ b/tools/quality/prettier.config.mjs @@ -0,0 +1,2 @@ +/** One formatting policy shared by authored code and the quality template itself. */ +export default { singleQuote: true, trailingComma: 'all' }; diff --git a/tools/quality/run.mjs b/tools/quality/run.mjs new file mode 100644 index 0000000..e0b21ae --- /dev/null +++ b/tools/quality/run.mjs @@ -0,0 +1,360 @@ +#!/usr/bin/env node +import { + readFileSync, + writeFileSync, + mkdirSync, + mkdtempSync, + rmSync, +} from 'node:fs'; +import { dirname, resolve, join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + execute, + baseRef, + baseFile, + currentFile, + git, + changedFiles, +} from './lib/git.mjs'; +import { assess, parseJson } from './lib/baseline.mjs'; +import { runGuards, validateConfig } from './lib/guards.mjs'; +import { + knipRecords, + duplicateRecords, + eslintRecords, + architectureRecords, + tokenFingerprint, +} from './lib/reports.mjs'; +import { historicalAnalysis } from './lib/historical.mjs'; + +const directory = dirname(fileURLToPath(import.meta.url)); +const args = process.argv.slice(2); +const action = args[0]; + +/** Read an option without treating subsequent flags as values. */ +function option(name, fallback) { + const position = args.indexOf(name); + if (position < 0) return fallback; + const value = args[position + 1]; + if (!value || value.startsWith('--')) + throw new Error(`Missing value for ${name}`); + return value; +} + +/** Record analyzer exit state together with the report; non-diagnostic exits always fail. */ +function analyze(command, argv, name) { + const result = execute(command, argv); + if (![0, 1].includes(result.status)) + throw new Error(`${name} failed (${result.status}): ${result.stderr}`); + if (result.stderr.trim()) process.stderr.write(result.stderr); + return { status: result.status, report: parseJson(result.stdout, name) }; +} + +/** Run Knip's actual production or full graph with its independent config and JSON reporter. */ +function unused(config, production) { + const argv = [ + join(directory, 'node_modules/knip/bin/knip.js'), + '--config', + config.knipConfig ?? 'knip.json', + '--reporter', + join(directory, 'lib/knip-reporter.mjs'), + ]; + if (production) argv.push('--production'); + const { report } = analyze(process.execPath, argv, 'Knip'); + return { findings: knipRecords(report), report }; +} + +/** Run duplicate analysis into a fresh temporary directory, rejecting empty or missing output. */ +function duplicates(config) { + if (!config.duplicatePaths?.length) + throw new Error('duplicatePaths must list authored runtime/tooling roots'); + const output = mkdtempSync(join(tmpdir(), '1msg-jscpd-')); + try { + const argv = [ + join(directory, 'node_modules/jscpd/run-jscpd.js'), + ...config.duplicatePaths, + '--reporters', + 'json', + '--output', + output, + '--mode', + 'mild', + '--min-lines', + '5', + '--min-tokens', + '50', + '--silent', + '--absolute', + '--format', + ( + config.duplicateFormats ?? [ + 'javascript', + 'typescript', + 'jsx', + 'tsx', + 'svelte', + ] + ).join(','), + ]; + if (config.duplicateIgnore?.length) + argv.push('--ignore', config.duplicateIgnore.join(',')); + const result = execute(process.execPath, argv); + if (result.status !== 0) + throw new Error( + `jscpd failed (${result.status}): ${result.stderr || result.stdout}`, + ); + const report = parseJson( + readFileSync(join(output, 'jscpd-report.json'), 'utf8'), + 'jscpd', + ); + return { findings: duplicateRecords(report), report }; + } finally { + rmSync(output, { recursive: true, force: true }); + } +} + +/** Confirm initial allowances describe existing source; freshly introduced problems cannot be bootstrapped. */ +function initialEvidence(finding, base, historical) { + const prior = historical.find((item) => item.key === finding.key); + if (!prior || finding.value > prior.value) + throw new Error(`Cannot baseline a new/increased finding: ${finding.key}`); + const evidence = {}; + for (const path of finding.paths ?? [finding.path]) { + if (!path) throw new Error(`Finding lacks source evidence: ${finding.key}`); + const before = baseFile(base, path); + const current = currentFile(path); + if (before === null || current === null) + throw new Error(`Cannot baseline a new/missing file: ${path}`); + if (path.endsWith('package.json') && finding.symbol) { + const old = JSON.parse(before); + const dependencies = { + ...old.dependencies, + ...old.devDependencies, + ...old.optionalDependencies, + }; + evidence[path] = + finding.symbol in dependencies + ? `dependency:${finding.symbol}:${dependencies[finding.symbol]}` + : tokenFingerprint(before); + } else { + evidence[path] = tokenFingerprint(before); + } + } + return evidence; +} + +/** Write explicit historical allowances locally, then validate the same artifact as CI. */ +function bootstrap(findings, path, base, historical) { + if (process.env.CI) throw new Error('Baseline bootstrap is forbidden in CI'); + const owner = option('--owner'); + const expires = option('--expires'); + const reason = option('--reason'); + const issue = option('--issue'); + if (!owner || !expires || !reason || !issue) + throw new Error( + 'Bootstrap requires --owner, --expires, --reason and --issue', + ); + const entries = findings.map((finding) => ({ + ...finding, + owner, + expires, + reason, + issue, + evidence: initialEvidence(finding, base, historical), + })); + const baseline = { schemaVersion: 1, adoptionBase: base, entries }; + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`); + return baseline; +} + +/** Ratchet individual debt against trusted Git history, including reductions and expired allowances. */ +function gate(config, name, result, base, analyzeBase) { + if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error('Invalid gate name'); + const path = `${config.baselineDir ?? 'quality/baselines'}/${name}.json`; + const raw = currentFile(path); + const old = baseFile(base, path); + const previous = old === null ? null : parseJson(old, 'trusted baseline'); + const historical = + previous || result.findings.length === 0 + ? [] + : historicalAnalysis(base, config, directory, analyzeBase).findings; + const baseline = args.includes('--bootstrap') + ? bootstrap(result.findings, path, base, historical) + : raw === null + ? { schemaVersion: 1, entries: [] } + : parseJson(raw, 'baseline'); + if (!previous && baseline.entries.length) { + if (baseline.adoptionBase !== base) + throw new Error('Initial baseline is not tied to the trusted base SHA'); + for (const entry of baseline.entries) { + if ( + JSON.stringify(entry.evidence) !== + JSON.stringify(initialEvidence(entry, base, historical)) + ) { + throw new Error(`Initial baseline evidence changed: ${entry.key}`); + } + } + } + const errors = assess(result.findings, baseline, previous); + if (name === 'duplicates') { + if ( + !Number.isFinite(config.duplicateMaxPercentage) || + config.duplicateMaxPercentage < 0 || + config.duplicateMaxPercentage > 100 + ) { + throw new Error( + 'duplicateMaxPercentage must specify the inventoried numeric ceiling (0–100)', + ); + } + const percentage = result.report.statistics.total.percentage; + if (!Number.isFinite(percentage)) + throw new Error('jscpd omitted its numeric duplicate percentage'); + if (percentage > config.duplicateMaxPercentage) + errors.push( + `Duplicate percentage ${percentage} exceeds ${config.duplicateMaxPercentage}`, + ); + } + mkdirSync('quality/reports', { recursive: true }); + writeFileSync( + `quality/reports/${name}.json`, + `${JSON.stringify( + { + schemaVersion: 1, + head: git(['rev-parse', 'HEAD']).trim(), + base, + result: errors.length ? 'FAIL' : 'PASS', + findings: result.findings, + errors, + analyzer: result.report, + }, + null, + 2, + )}\n`, + ); + if (errors.length) throw new Error(errors.join('\n')); + console.log( + `${name}: PASS (${result.findings.length} existing, individually tracked violations)`, + ); +} + +/** Dispatch one mandatory check; unsupported actions or missing configuration never count as success. */ +async function main() { + const config = ( + await import(pathToFileURL(resolve('quality.config.mjs')).href) + ).default; + validateConfig(config); + const base = baseRef( + config.defaultBranch, + option('--base', process.env.QUALITY_BASE), + ); + if (action === 'changed-files') { + console.log(JSON.stringify(changedFiles(base))); + return; + } + if (action === 'guards') { + const result = runGuards(config, base); + if (result.errors.length) throw new Error(result.errors.join('\n')); + console.log( + `guards: PASS (${result.checked} authored source files inventoried)`, + ); + return; + } + if (['unused-production', 'unused-full'].includes(action)) { + const analyzeBase = (sourceConfig) => + unused(sourceConfig, action === 'unused-production'); + gate(config, action, analyzeBase(config), base, analyzeBase); + return; + } + if (action === 'duplicates') { + gate(config, action, duplicates(config), base, duplicates); + return; + } + if (action === 'duplicates-full') { + if (!config.duplicateFullPaths?.length) + throw new Error( + 'duplicateFullPaths must inventory runtime, tests and tooling', + ); + const result = duplicates({ + ...config, + duplicatePaths: config.duplicateFullPaths, + duplicateIgnore: + config.duplicateFullIgnore ?? + (config.generated ?? []).map((entry) => entry.pattern), + }); + mkdirSync('quality/reports', { recursive: true }); + writeFileSync( + 'quality/reports/duplicates-full.json', + `${JSON.stringify( + { + schemaVersion: 1, + head: git(['rev-parse', 'HEAD']).trim(), + base, + result: 'REPORT', + ...result, + }, + null, + 2, + )}\n`, + ); + console.log( + `duplicates-full: analyzed ${result.report.statistics.total.sources} files; report saved`, + ); + return; + } + if (action === 'report' && option('--kind') === 'eslint') { + const split = args.indexOf('--'); + if (split < 0 || !args[split + 1]) + throw new Error('report requires -- '); + const analyzeBase = () => { + const { report } = analyze( + args[split + 1], + args.slice(split + 2), + 'ESLint', + ); + return { findings: eslintRecords(report), report }; + }; + gate( + config, + option('--name', 'complexity'), + analyzeBase(), + base, + analyzeBase, + ); + return; + } + if (action === 'report' && option('--kind') === 'depcruiser') { + const split = args.indexOf('--'); + if (split < 0 || !args[split + 1]) + throw new Error('report requires -- '); + const analyzeBase = () => { + const result = execute(args[split + 1], args.slice(split + 2)); + const report = parseJson(result.stdout, 'dependency-cruiser'); + if ( + !Number.isInteger(report.summary?.advisedExitCode) || + (result.status !== 0 && + result.status !== report.summary.advisedExitCode % 256) + ) { + throw new Error( + `dependency-cruiser exit/report mismatch (${result.status}): ${result.stderr}`, + ); + } + return { findings: architectureRecords(report), report }; + }; + gate( + config, + option('--name', 'architecture'), + analyzeBase(), + base, + analyzeBase, + ); + return; + } + throw new Error(`Unknown quality action: ${action}`); +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); diff --git a/tools/quality/seal.mjs b/tools/quality/seal.mjs new file mode 100644 index 0000000..8e2c5b9 --- /dev/null +++ b/tools/quality/seal.mjs @@ -0,0 +1,19 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { digest } from './lib/baseline.mjs'; +import { templateFiles } from './lib/template.mjs'; + +// Maintainers seal a reviewed template version before copying it into consumer repositories. +const root = dirname(fileURLToPath(import.meta.url)); +const { version } = JSON.parse(readFileSync(`${root}/package.json`, 'utf8')); +const files = Object.fromEntries( + templateFiles(root).map((path) => [ + path, + digest(readFileSync(`${root}/${path}`, 'utf8')), + ]), +); +writeFileSync( + `${root}/template-manifest.json`, + `${JSON.stringify({ schemaVersion: 1, version, files }, null, 2)}\n`, +); diff --git a/tools/quality/template-manifest.json b/tools/quality/template-manifest.json new file mode 100644 index 0000000..444e17d --- /dev/null +++ b/tools/quality/template-manifest.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "version": "1.0.2", + "files": { + "README.md": "1d3f6f9d420adea402d92aff483c14271e6e24c291437d6ea5b59c3162702a20", + "eslint.mjs": "607ddfaac3d365fedd7dbc5e21c781d448347b1c36662f3492e4364ff9f22ded", + "lib/baseline.mjs": "7876ba90b80448d9c9003c21e348095ed22630c41fc26ed944984742020549dd", + "lib/git.mjs": "b7c26871526d864bf8d12f01c613d3bfa0adb2da9d072544d8f09407ef598758", + "lib/guards.mjs": "310c224c5d072fa16b1cf5f0c47e5da4258f0f63fa9a1c4dc114c9fe192bc398", + "lib/historical.mjs": "d338d88bae6d1da19f27296fbb6c57cff4ca09664fe7baa8d6c8d801ca3e69f2", + "lib/knip-reporter.mjs": "175d7d96e2913e8f6de04b6c706172258d9d7654a26ba7f9ed7edff79d3fc8ca", + "lib/reports.mjs": "f47f6cbf8022691e61a245bf54a5fdd1ba9f2037906ec6576f2ac771224161dc", + "lib/template.mjs": "c2a14b715f0c9dbe8b8243b142f4c6628967400fb8f92767537169e1b7bfd8f4", + "package-lock.json": "931d28ffff6590892a072569df9c57345d0a388a22598b3482dee53136a52828", + "package.json": "b2c70073c423135e4b5abba9507d58fd5136a676339e816b72d6dd4bcbb1b757", + "prettier.config.mjs": "7576563a9a30aff8eb3177f9cc2e4bc7a0f6b825042eadb9a619b86f519c5e5e", + "run.mjs": "b60c0fc2419c8cfd32eddf369da38b2d87e2b7eacbc31fda31c598930900f199", + "seal.mjs": "fb38c8e08ea3cf9f4dcd95e45ffc931aea3b7a54181fed88d7379f8aad4c050d", + "test/cli.test.mjs": "f743a18f0763cf5b5f42f8d2a4b71f67a4de900a3955a299309395af95b99cf3", + "test/ratchet.test.mjs": "62fef1cc7a1851db81c30966909968ff84637b8ed8003821120269c4fae70d49", + "tsconfig.next.json": "d900a35fcb0852376b06854df5be2f8f178e26cb5383b93b07ebeb6220e30faf", + "tsconfig.node.json": "627661abcb7e992c4bfaf98d6e68a12678573b5d4f6e821ea0fa43fa6e233f28", + "tsconfig.svelte.json": "5777247ea98aaf2d886b87cd12206a641377edcdd086ce5fd5b58adc6f0bc771" + } +} diff --git a/tools/quality/test/cli.test.mjs b/tools/quality/test/cli.test.mjs new file mode 100644 index 0000000..897d9cd --- /dev/null +++ b/tools/quality/test/cli.test.mjs @@ -0,0 +1,510 @@ +import { templateFiles } from '../lib/template.mjs'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + mkdtempSync, + mkdirSync, + writeFileSync, + rmSync, + cpSync, + readFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const runner = fileURLToPath(new URL('../run.mjs', import.meta.url)); + +/** Create a real Git fixture so CLI tests exercise trusted base and working-tree changes together. */ +function fixture() { + const cwd = mkdtempSync(join(tmpdir(), '1msg-quality-test-')); + mkdirSync(join(cwd, 'src')); + writeFileSync( + join(cwd, 'package.json'), + '{"name":"quality-fixture","private":true,"type":"module"}\n', + ); + writeFileSync( + join(cwd, 'quality.config.mjs'), + `export default { + schemaVersion:1,profile:'A',defaultBranch:'main',sourceRoots:['src','quality.config.mjs'], + knipConfig:'knip.json',duplicatePaths:['src'],duplicateMaxPercentage:100 + };\n`, + ); + writeFileSync( + join(cwd, 'knip.json'), + JSON.stringify({ entry: ['src/index.js!'], project: ['src/**/*.js!'] }), + ); + const git = (...args) => { + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + git('init', '-b', 'main'); + git('config', 'user.email', 'quality@example.invalid'); + git('config', 'user.name', 'Quality fixture'); + const commit = () => { + git('add', '.'); + git('commit', '-m', 'test: fixture'); + return git('rev-parse', 'HEAD'); + }; + const run = (...args) => + spawnSync(process.execPath, [runner, ...args], { + cwd, + encoding: 'utf8', + env: { ...process.env, CI: '', QUALITY_BASE: undefined }, + timeout: 60_000, + maxBuffer: 16 * 1024 * 1024, + }); + return { + cwd, + git, + commit, + run, + write: (path, text) => writeFileSync(join(cwd, path), text), + close: () => rmSync(cwd, { recursive: true, force: true }), + }; +} + +test('real CLI blocks undocumented new functions, broad suppression and uncovered files', () => { + const f = fixture(); + try { + f.write('src/index.js', 'console.log("app");\n'); + const base = f.commit(); + assert.equal(f.run('guards', '--base', base).status, 0); + f.write( + 'src/index.js', + 'function createWidget() { return 2; }\nconsole.log(createWidget());\n', + ); + assert.match( + f.run('guards', '--base', base).stderr, + /document createWidget/, + ); + f.write( + 'src/index.js', + '/** Construct the default widget. */\nfunction createWidget() { return 2; }\nconsole.log(createWidget());\n', + ); + assert.equal(f.run('guards', '--base', base).status, 0); + f.write('src/index.js', '// @ts-nocheck\nconsole.log("app");\n'); + assert.match(f.run('guards', '--base', base).stderr, /broad lint/); + f.write('rogue.js', 'console.log("forgotten");\n'); + assert.match( + f.run('guards', '--base', base).stderr, + /no declared source root/, + ); + assert.notEqual(f.run('guards', '--base', 'missing-ref').status, 0); + } finally { + f.close(); + } +}); + +test('actual Knip CLI blocks an unused file and unresolved import', () => { + const f = fixture(); + try { + f.write('src/index.js', 'console.log("app");\n'); + const base = f.commit(); + assert.equal(f.run('unused-full', '--base', base).status, 0); + f.write('src/forgotten.js', 'export const value = 42;\n'); + const unused = f.run('unused-full', '--base', base); + assert.equal(unused.status, 1, unused.stderr); + assert.match(unused.stderr, /forgotten/); + f.write( + 'src/index.js', + 'import value from "./missing.js"; console.log(value);\n', + ); + assert.match( + f.run('unused-production', '--base', base).stderr, + /unresolved/, + ); + } finally { + f.close(); + } +}); + +test('documentation survives formatting and unrelated callback removal, but new copies still require docs', () => { + const f = fixture(); + try { + f.write( + 'src/index.js', + ` + run(() => { return {name: "removed"}; }); + run(() => { return {name: "retained"}; }); + function load(value) { return request(value, {name: "widget"}); } + `, + ); + const base = f.commit(); + f.write( + 'src/index.js', + ` + run(() => { return {name: 'retained',}; }); + function load(value,) { return request(value, {name: 'widget',},); } + `, + ); + const formatted = f.run('guards', '--base', base); + assert.equal(formatted.status, 0, formatted.stderr); + f.write( + 'src/index.js', + ` + run(() => { return {name: 'retained',}; }); + run(() => { return {name: 'retained',}; }); + function load(value,) { return request(value, {name: 'widget',},); } + `, + ); + const duplicated = f.run('guards', '--base', base); + assert.equal(duplicated.status, 1, duplicated.stderr); + assert.match(duplicated.stderr, /document /); + } finally { + f.close(); + } +}); + +test('actual jscpd CLI blocks a new clone even with unrelated unique lines', () => { + const f = fixture(); + try { + const source = `export function summarize(values) { + const result = []; + for (const value of values) { + const normalized = String(value).trim(); + if (normalized.length > 0) { + result.push({ value: normalized, length: normalized.length }); + } + } + return result.sort((left, right) => left.length - right.length); + }\n`; + f.write('src/index.js', source); + const base = f.commit(); + const clean = f.run('duplicates', '--base', base); + assert.equal(clean.status, 0, clean.stderr); + f.write( + 'src/copy.js', + source + '\nexport const unrelated = "extra unique content";\n', + ); + const duplicated = f.run('duplicates', '--base', base); + assert.equal(duplicated.status, 1, duplicated.stderr); + assert.match(duplicated.stderr, /New or increased/); + } finally { + f.close(); + } +}); + +test('Knip production rejects runtime code referenced only from tests while full accepts it', () => { + const f = fixture(); + try { + f.write( + 'knip.json', + JSON.stringify({ + entry: ['src/index.js!', 'test.js'], + project: ['src/**/*.js!', 'test.js'], + }), + ); + f.write('src/index.js', 'console.log("app");\n'); + f.write('src/helper.js', 'export const value = 42;\n'); + f.write( + 'test.js', + 'import { value } from "./src/helper.js"; console.log(value);\n', + ); + const base = f.commit(); + const full = f.run('unused-full', '--base', base); + assert.equal(full.status, 0, full.stderr); + const production = f.run('unused-production', '--base', base); + assert.equal(production.status, 1, production.stderr); + assert.match(production.stderr, /helper/); + } finally { + f.close(); + } +}); + +test('initial debt is replayed against trusted source, including edited files and newly disconnected imports', () => { + const f = fixture(); + const bootstrap = [ + '--bootstrap', + '--owner', + 'quality', + '--expires', + '2099-01-01', + '--reason', + 'historical fixture debt', + '--issue', + 'TEST-1', + ]; + try { + f.write( + 'src/index.js', + 'import { value } from "./helper.js"; console.log(value);\n', + ); + f.write('src/helper.js', 'export const value = 42;\n'); + f.write('src/forgotten.js', 'export const oldDebt = 1;\n'); + const base = f.commit(); + f.write('src/forgotten.js', 'export const oldDebt = 2;\n'); + f.write('new-tool.mjs', 'console.log(123);'); + f.write( + 'knip.json', + JSON.stringify({ + entry: ['src/index.js!', 'new-tool.mjs'], + project: ['src/**/*.js!', 'new-tool.mjs'], + }), + ); + const historical = f.run('unused-full', '--base', base, ...bootstrap); + assert.equal(historical.status, 0, historical.stderr); + f.write('src/index.js', 'console.log("now disconnected");\n'); + const newDebt = f.run('unused-full', '--base', base, ...bootstrap); + assert.equal(newDebt.status, 1, newDebt.stderr); + assert.match( + newDebt.stderr, + /Cannot baseline a new\/increased finding.*helper/, + ); + } finally { + f.close(); + } +}); + +test('jscpd preserves multiple source roots and enforces its numeric ceiling', () => { + const f = fixture(); + try { + mkdirSync(join(f.cwd, 'runtime')); + const source = `export function summarize(values) { + const result = []; + for (const value of values) { + const normalized = String(value).trim(); + if (normalized.length > 0) result.push({ value: normalized, length: normalized.length }); + } + return result.sort((left, right) => left.length - right.length); + }\n`; + f.write('src/index.js', source); + f.write('runtime/copy.js', source); + f.write( + 'quality.config.mjs', + `export default { schemaVersion:1,profile:'A',sourceRoots:['src','runtime'], + duplicatePaths:['src','runtime'],duplicateMaxPercentage:100 };`, + ); + const base = f.commit(); + const bootstrap = [ + '--bootstrap', + '--owner', + 'quality', + '--expires', + '2099-01-01', + '--reason', + 'historical fixture debt', + '--issue', + 'TEST-1', + ]; + const accepted = f.run('duplicates', '--base', base, ...bootstrap); + assert.equal(accepted.status, 0, accepted.stderr); + f.write( + 'quality.config.mjs', + `export default { schemaVersion:1,profile:'A',sourceRoots:['src','runtime'], + duplicatePaths:['src','runtime'],duplicateMaxPercentage:0 };`, + ); + const percentage = f.run('duplicates', '--base', base); + assert.equal(percentage.status, 1, percentage.stderr); + assert.match(percentage.stderr, /Duplicate percentage/); + } finally { + f.close(); + } +}); + +test('actual dependency-cruiser detects a long runtime cycle and refuses native I/O debt', () => { + const f = fixture(); + const executable = fileURLToPath( + new URL( + '../node_modules/dependency-cruiser/bin/dependency-cruiser.mjs', + import.meta.url, + ), + ); + const command = [ + '--', + process.execPath, + executable, + 'src', + '--config', + 'dependency-cruiser.config.cjs', + '--output-type', + 'json', + ]; + try { + f.write( + 'dependency-cruiser.config.cjs', + `module.exports = { forbidden: [ + {name:'no-runtime-cycles',severity:'error',from:{},to:{circular:true,dependencyTypesNot:['type-only']}}, + {name:'application-no-native-io',severity:'error',from:{path:'src'},to:{dependencyTypes:['core']}}, + ] };`, + ); + f.write('src/index.js', 'import "./second.js"; console.log("one");\n'); + f.write('src/second.js', 'import "./third.js"; console.log("two");\n'); + f.write('src/third.js', 'console.log("three");\n'); + const base = f.commit(); + const clean = f.run( + 'report', + '--kind', + 'depcruiser', + '--base', + base, + ...command, + ); + assert.equal(clean.status, 0, clean.stderr); + f.write('src/third.js', 'import "./index.js"; console.log("three");\n'); + const cycle = f.run( + 'report', + '--kind', + 'depcruiser', + '--base', + base, + ...command, + ); + assert.equal(cycle.status, 1, cycle.stderr); + assert.match(cycle.stderr, /no-runtime-cycles/); + f.write('src/third.js', 'import fs from "node:fs"; console.log(fs);\n'); + const native = f.run( + 'report', + '--kind', + 'depcruiser', + '--base', + base, + '--bootstrap', + '--owner', + 'quality', + '--expires', + '2099-01-01', + '--reason', + 'cannot bless native dependency', + '--issue', + 'TEST-1', + ...command, + ); + assert.equal(native.status, 1, native.stderr); + assert.match(native.stderr, /Non-baselinable architecture/); + } finally { + f.close(); + } +}); + +test('vendored exemption rejects unregistered and modified template files', () => { + const f = fixture(); + try { + f.write('src/index.js', 'console.log("app");\n'); + const base = f.commit(); + const template = fileURLToPath(new URL('..', import.meta.url)); + for (const path of [...templateFiles(template), 'template-manifest.json']) { + const destination = join(f.cwd, 'tools/quality', path); + mkdirSync(dirname(destination), { recursive: true }); + cpSync(join(template, path), destination); + } + const clean = f.run('guards', '--base', base); + assert.equal(clean.status, 0, clean.stderr); + f.write('tools/quality/hidden.js', 'export const hidden = 1;'); + assert.match(f.run('guards', '--base', base).stderr, /undeclared/); + rmSync(join(f.cwd, 'tools/quality/hidden.js')); + f.write('tools/quality/README.md', 'unreviewed mutation'); + assert.match(f.run('guards', '--base', base).stderr, /integrity mismatch/); + } finally { + f.close(); + } +}); + +/** Real embedded Svelte clones keep language identity but historical evidence belongs to their physical component. */ +test('jscpd Svelte fragments bootstrap existing clones and reject another participant', () => { + const f = fixture(); + try { + const component = ` +

Summary

\n`; + f.write('src/First.svelte', component); + f.write('src/Second.svelte', component); + const base = f.commit(); + const accepted = f.run( + 'duplicates', + '--base', + base, + '--bootstrap', + '--owner', + 'quality', + '--expires', + '2099-01-01', + '--reason', + 'historical Svelte fixture debt', + '--issue', + 'TEST-1', + ); + assert.equal(accepted.status, 0, accepted.stderr); + const report = JSON.parse( + readFileSync(join(f.cwd, 'quality/reports/duplicates.json'), 'utf8'), + ); + assert.ok( + report.findings.some((finding) => + finding.participants?.some((path) => + path.endsWith('.svelte:javascript'), + ), + ), + ); + assert.ok( + report.findings.every((finding) => + finding.paths.every((path) => path.endsWith('.svelte')), + ), + ); + f.write('src/Third.svelte', component); + const expanded = f.run('duplicates', '--base', base); + assert.equal(expanded.status, 1, expanded.stderr); + assert.match(expanded.stderr, /New or increased/); + } finally { + f.close(); + } +}); + +/** Only the new sealed vendor exemption is omitted from historical graphs; authored stale ignores still fail. */ +test('historical Knip removes a current-only exact vendor ignore without relaxing authored coverage', () => { + const f = fixture(); + try { + f.write('src/index.js', 'console.log("app");\n'); + f.write('src/forgotten.js', 'export const historicalDebt = 1;\n'); + const base = f.commit(); + const template = fileURLToPath(new URL('..', import.meta.url)); + for (const path of [...templateFiles(template), 'template-manifest.json']) { + const destination = join(f.cwd, 'tools/quality', path); + mkdirSync(dirname(destination), { recursive: true }); + cpSync(join(template, path), destination); + } + f.write( + 'quality.mjs', + 'import { record } from "./tools/quality/lib/baseline.mjs"; console.log(record("example"));\n', + ); + const config = { + entry: ['src/index.js!', 'quality.mjs'], + project: ['src/**/*.js!', '**/*.mjs'], + ignore: ['tools/quality/**'], + }; + f.write('knip.json', JSON.stringify(config)); + const accepted = f.run( + 'unused-full', + '--base', + base, + '--bootstrap', + '--owner', + 'quality', + '--expires', + '2099-01-01', + '--reason', + 'historical source debt', + '--issue', + 'TEST-1', + ); + assert.equal(accepted.status, 0, accepted.stderr); + config.ignore.push('src/missing/**'); + f.write('knip.json', JSON.stringify(config)); + const invalid = f.run('unused-full', '--base', base); + assert.equal(invalid.status, 1, invalid.stderr); + assert.match(invalid.stderr, /stale exclusion failure.*src\/missing/); + } finally { + f.close(); + } +}); diff --git a/tools/quality/test/ratchet.test.mjs b/tools/quality/test/ratchet.test.mjs new file mode 100644 index 0000000..7722924 --- /dev/null +++ b/tools/quality/test/ratchet.test.mjs @@ -0,0 +1,238 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { assess, parseJson } from '../lib/baseline.mjs'; +import { + knipRecords, + duplicateRecords, + eslintRecords, + tokenFingerprint, +} from '../lib/reports.mjs'; +import { functionsIn, suppressionErrors } from '../lib/guards.mjs'; + +const owner = { + owner: 'quality-maintainers', + reason: 'Existing complexity', + issue: '1MSG-156', + expires: '2026-11-12', +}; +const baseline = (entries) => ({ schemaVersion: 1, entries }); +const allowance = (key, value) => ({ key, value, ...owner }); + +test('growth cannot be offset by reductions, and reductions retire allowances', () => { + const old = baseline([allowance('a', 25), allowance('b', 20)]); + assert.match( + assess( + [ + { key: 'a', value: 26 }, + { key: 'b', value: 15 }, + ], + old, + old, + '2026-10-12', + ).join('\n'), + /increased/, + ); + assert.match( + assess( + [ + { key: 'a', value: 25 }, + { key: 'b', value: 15 }, + ], + old, + old, + '2026-10-12', + ).join('\n'), + /Reduce/, + ); +}); + +test('new or increased baselines, expiry extensions and stale allowances fail', () => { + const old = baseline([allowance('a', 25)]); + const next = baseline([ + { ...allowance('a', 26), expires: '2026-12-12' }, + allowance('new', 1), + ]); + const errors = assess( + [{ key: 'a', value: 26 }], + next, + old, + '2026-10-12', + ).join('\n'); + assert.match(errors, /expanded/); + assert.match(errors, /expiry extended/); + assert.match(errors, /stale/); + assert.match(assess([], old, old, '2026-12-01').join('\n'), /Expired/); +}); + +test('empty or malformed analyzer reports are not successful checks', () => { + assert.throws(() => parseJson('Killed', 'Knip'), /complete JSON/); + assert.throws(() => knipRecords({}), /schema/); + assert.throws( + () => + duplicateRecords({ + duplicates: [], + statistics: { total: { sources: 0 } }, + }), + /empty/, + ); + assert.throws(() => eslintRecords([]), /empty/); +}); + +test('Knip distinguishes unresolved imports, unused dependencies and exported symbols', () => { + const rows = knipRecords({ + issues: [ + { + file: 'src/a.ts', + exports: [{ name: 'thing', line: 3 }], + unresolved: [{ name: './missing', line: 1 }], + files: [], + }, + { file: 'package.json', dependencies: [{ name: 'unused' }] }, + ], + }); + assert.equal(rows.length, 3); + assert.ok(rows.some((row) => row.rule === 'unresolved')); + const moved = knipRecords({ + issues: [{ file: 'src/a.ts', exports: [{ name: 'thing', line: 99 }] }], + }); + assert.equal(rows.find((row) => row.rule === 'exports').key, moved[0].key); +}); + +test('duplicate identity ignores line moves but notices additional participants and extended code', () => { + const clone = { + format: 'typescript', + fragment: 'const value = input + 3;', + tokens: 7, + firstFile: { name: 'src/a.ts', start: 1, end: 2 }, + secondFile: { name: 'src/b.ts', start: 7, end: 8 }, + }; + const report = (clones) => ({ + duplicates: clones, + statistics: { total: { sources: 3 } }, + }); + const original = duplicateRecords(report([clone])); + const moved = duplicateRecords( + report([ + { ...clone, firstFile: { ...clone.firstFile, start: 9, end: 10 } }, + ]), + ); + assert.equal(original[0].key, moved[0].key); + assert.notEqual( + original[0].key, + duplicateRecords( + report([clone, { ...clone, secondFile: { name: 'src/c.ts' } }]), + )[0].key, + ); + assert.notEqual( + original[0].key, + duplicateRecords( + report([{ ...clone, fragment: clone.fragment + 'return value;' }]), + )[0].key, + ); + assert.equal( + tokenFingerprint('const a = 1;'), + tokenFingerprint('/* moved */ const a=1;'), + ); + assert.notEqual( + tokenFingerprint('const a="a b";'), + tokenFingerprint('const a="ab";'), + ); +}); + +test('documentation covers private methods, arrow helpers and Svelte scripts', () => { + const source = + '/** Resolve the current owner. */\nconst owner = () => 1;\nclass Foo {\n/** Release the acquired resource. */\nprivate release() {}\n}'; + const functions = functionsIn('src/a.ts', source); + assert.equal(functions.length, 2); + assert.ok(functions.every((fn) => fn.comments.length)); + assert.equal( + functionsIn( + 'View.svelte', + '

Hi

', + ).length, + 1, + ); + assert.throws( + () => functionsIn('broken.ts', 'function f( {'), + /Cannot parse/, + ); +}); + +test('documentation fingerprint preserves array holes and changes to callback ownership', () => { + const signature = (source) => functionsIn('src/a.ts', source)[0].signature; + assert.equal( + signature('function f() { return [1]; }'), + signature('function f() { return [1,]; }'), + ); + assert.notEqual( + signature('function f() { return []; }'), + signature('function f() { return [,]; }'), + ); + assert.notEqual( + signature('function f() { return [1]; }'), + signature('function f() { return [1,,]; }'), + ); + assert.notEqual( + functionsIn('src/a.ts', 'first(() => { return 1; });')[0].key, + functionsIn('src/a.ts', 'second(() => { return 1; });')[0].key, + ); +}); + +test('whole-file line limits keep their identity when the diagnostic moves between functions', () => { + const finding = (line) => + eslintRecords([ + { + filePath: 'lib/guards.mjs', + messages: [ + { + ruleId: 'max-lines', + message: 'File has too many lines (600). Maximum allowed is 500.', + line, + column: 1, + }, + ], + }, + ])[0]; + assert.equal(finding(20).key, 'lib/guards.mjs|max-lines||0'); + assert.equal(finding(120).key, finding(20).key); +}); + +test('suppression gates do not mistake strings for directives', () => { + assert.equal( + suppressionErrors('a.ts', 'const example = "// @ts-ignore";', {}).length, + 0, + ); + assert.equal( + suppressionErrors('a.ts', '// @ts-nocheck\nexport const a = 1;', {}).length, + 1, + ); + assert.equal( + suppressionErrors('a.ts', '/* eslint-disable */\nexport const a = 1;', {}) + .length, + 1, + ); + assert.equal( + suppressionErrors( + 'a.ts', + '// eslint-disable-next-line no-alert -- Temporary browser compatibility\nalert(1);', + {}, + ).length, + 0, + ); + assert.equal( + suppressionErrors( + 'a.ts', + '// @ts-expect-error: Verify rejected primitive input\nf(1);', + {}, + ).length, + 1, + ); + assert.equal( + suppressionErrors( + 'negative.test.ts', + '// @ts-expect-error: Verify rejected primitive input\nf(1);', + { negativeTypeTests: ['*.test.ts'] }, + ).length, + 0, + ); +}); diff --git a/tools/quality/tsconfig.next.json b/tools/quality/tsconfig.next.json new file mode 100644 index 0000000..583277d --- /dev/null +++ b/tools/quality/tsconfig.next.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "noEmit": true, + "isolatedModules": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmitOnError": true, + "plugins": [{ "name": "next" }] + } +} diff --git a/tools/quality/tsconfig.node.json b/tools/quality/tsconfig.node.json new file mode 100644 index 0000000..cba7925 --- /dev/null +++ b/tools/quality/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmitOnError": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/tools/quality/tsconfig.svelte.json b/tools/quality/tsconfig.svelte.json new file mode 100644 index 0000000..da3596a --- /dev/null +++ b/tools/quality/tsconfig.svelte.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "isolatedModules": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmitOnError": true + } +} diff --git a/tsconfig.json b/tsconfig.json index ac8472c..c2089e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "module": "commonjs", + "module": "NodeNext", "lib": ["ES2020"], "declaration": true, "declarationMap": true, @@ -10,9 +10,12 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", + "moduleResolution": "NodeNext", "rootDir": "src", - "outDir": "dist" + "outDir": "dist", + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmitOnError": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "src/**/__tests__/**"] diff --git a/tsconfig.quality.json b/tsconfig.quality.json new file mode 100644 index 0000000..946964a --- /dev/null +++ b/tsconfig.quality.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmitOnError": true, + "rootDir": "." + }, + "include": [ + "src/**/*.ts", + "tests/**/*.ts", + "vitest.config.ts", + "nodes/**/*.ts", + "utils/**/*.ts", + "credentials/**/*.ts" + ], + "exclude": ["node_modules", "dist"] +} From 833a0d028f22a439d05e79dae3b91487958001f2 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Mon, 12 Oct 2026 20:54:34 +0500 Subject: [PATCH 02/11] style: format replayed duplicate baseline --- quality/baselines/duplicates.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/quality/baselines/duplicates.json b/quality/baselines/duplicates.json index f32ae4e..47cd30f 100644 --- a/quality/baselines/duplicates.json +++ b/quality/baselines/duplicates.json @@ -5,9 +5,7 @@ { "key": "typescript|3b0d1d2c147385a1c51f10b414c0d2b850172427221482b7fc1c01a7983ad11f|src/http.ts", "value": 1, - "paths": [ - "src/http.ts" - ], + "paths": ["src/http.ts"], "tokens": 52, "owner": "1MSG SDK/tooling maintainers", "expires": "2026-12-11", @@ -20,10 +18,7 @@ { "key": "typescript|e6285bd27db1ab4139f646c8d0b7e9cdee896370ed43b11de68d4b86ec996443|src/http.ts|src/index.ts", "value": 1, - "paths": [ - "src/http.ts", - "src/index.ts" - ], + "paths": ["src/http.ts", "src/index.ts"], "tokens": 61, "owner": "1MSG SDK/tooling maintainers", "expires": "2026-12-11", From 05b03cebb9ae5f41645df411d1366609151575be Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Mon, 12 Oct 2026 21:06:43 +0500 Subject: [PATCH 03/11] chore: pin MCP signature inputs to validated SDK producer --- quality/mcp/provenance.json | 11 +-- quality/mcp/vendor/apis/FlowsApi.ts | 20 ++--- quality/mcp/vendor/apis/GroupsApi.ts | 8 +- quality/mcp/vendor/apis/MessagingApi.ts | 100 ++++++++++++------------ quality/mcp/vendor/apis/TemplatesApi.ts | 10 +-- 5 files changed, 75 insertions(+), 74 deletions(-) diff --git a/quality/mcp/provenance.json b/quality/mcp/provenance.json index 76f419b..c52ef0f 100644 --- a/quality/mcp/provenance.json +++ b/quality/mcp/provenance.json @@ -7,16 +7,17 @@ "CallingApi.ts": "129eb7db5b92150022f9da64dae6b85a7db267ae3478e1979f8d71583db458e0", "CatalogApi.ts": "b85c2c5a0f61f6cfbfca720a1e0c03a5b9abf7b680cec6b420705329337f7fc8", "ChannelApi.ts": "a969213513f911a04bbce46357591e9c46220d8b474e075303bfd54b3b6123ff", - "FlowsApi.ts": "c4834948439172c986483c8a3110f49a4d138fa262be8452dba34404b85d8c15", - "GroupsApi.ts": "ca771e833b3506b247bf9c1c32a194573d1ef72fe0ee1ca8e8b8ca6ffedb662b", - "MessagingApi.ts": "9d57d04fbf78687b94a5333922dc843b62f25a83bf779a34722fdd2588c646d2", + "FlowsApi.ts": "d1b1ea3d467f99816eb7b888a66d13801a90981419d0f4051b2579c195f8286d", + "GroupsApi.ts": "3e5af239ed0272f900480f5c87bfa86f3ac78676d19eb21f659f166bc92cab6f", + "MessagingApi.ts": "df6b1fbf5b75516495270882d1ce0046e0e6e253f25314e3f823f669f95d6c1b", "ProfileApi.ts": "b11f725dea52f2afe672b2b6413346f529feb315d336d68712b2f50f5403d217", - "TemplatesApi.ts": "15043c2f7ef1952982681105cc83d4599222feeeb1029ef83e65692b72495639", + "TemplatesApi.ts": "9221bc60d79e200c94328a0b175c93d3336050b5154ec8a0d5050a466c385ee8", "UsersApi.ts": "65cbbf11185465c4e8a0a82faa0e62f61f681a449d78f73c1714b9fa1639a5b6", "WebhooksApi.ts": "a64d716467e1643463f0d0c1a508938a0cd0ab4a16bd1e1bf0e8adb9a5df4208" }, "contractSourceCommit": "888ee40a635ab19dbff4c101b8d4329790c7254f", "producerTask": "https://plane.1msg.io/1msg/browse/1MSG-156/", "producerSource": "1msg-api/gen/generate-mcp-tools.js with the reviewed 1MSG-156 numeric media ID compatibility patch", - "signatureProducer": "OpenAPI Generator 7.22.0 plus 1msg-api TypeScript compatibility transforms, same pinned public contract" + "signatureProducer": "OpenAPI Generator 7.22.0 plus 1msg-api TypeScript compatibility and exact-optional transforms, same pinned public contract", + "producerSourceCommit": "a487c6172c269b84e779b2553cd44161e6973214" } diff --git a/quality/mcp/vendor/apis/FlowsApi.ts b/quality/mcp/vendor/apis/FlowsApi.ts index 3348935..a27d24f 100644 --- a/quality/mcp/vendor/apis/FlowsApi.ts +++ b/quality/mcp/vendor/apis/FlowsApi.ts @@ -77,37 +77,37 @@ import { export interface CreateFlowsOperationRequest { token: string; createFlowsRequest: CreateFlowsRequest; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface CreateFlowsFlowIdDeprecateRequest { flowId: string; token: string; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface CreateFlowsFlowIdPublishRequest { flowId: string; token: string; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface DeleteFlowsFlowIdRequest { flowId: string; token: string; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface GetFlowsFlowIdRequest { flowId: string; token: string; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface GetFlowsFlowIdPreviewRequest { flowId: string; token: string; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface GetWhatsappBusinessEncryptionRequest { @@ -116,22 +116,22 @@ export interface GetWhatsappBusinessEncryptionRequest { export interface ListFlowsRequest { token: string; - wabaAccountId?: string; - fields?: string; + wabaAccountId?: (string) | undefined; + fields?: (string) | undefined; } export interface PatchFlowsFlowIdAssetsOperationRequest { flowId: string; token: string; patchFlowsFlowIdAssetsRequest: PatchFlowsFlowIdAssetsRequest; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface PatchFlowsFlowIdMetadataOperationRequest { flowId: string; token: string; patchFlowsFlowIdMetadataRequest: PatchFlowsFlowIdMetadataRequest; - wabaAccountId?: string; + wabaAccountId?: (string) | undefined; } export interface SetWhatsappBusinessEncryptionOperationRequest { diff --git a/quality/mcp/vendor/apis/GroupsApi.ts b/quality/mcp/vendor/apis/GroupsApi.ts index 2b770cc..a0696ff 100644 --- a/quality/mcp/vendor/apis/GroupsApi.ts +++ b/quality/mcp/vendor/apis/GroupsApi.ts @@ -58,7 +58,7 @@ export interface DeleteGroupsGroupIdRequest { export interface GetGroupsGroupIdRequest { groupId: string; token: string; - fields?: string; + fields?: (string) | undefined; } export interface GetGroupsGroupIdInvitelinkRequest { @@ -68,9 +68,9 @@ export interface GetGroupsGroupIdInvitelinkRequest { export interface ListGroupsRequest { token: string; - limit?: number; - before?: string; - after?: string; + limit?: (number) | undefined; + before?: (string) | undefined; + after?: (string) | undefined; } /** diff --git a/quality/mcp/vendor/apis/MessagingApi.ts b/quality/mcp/vendor/apis/MessagingApi.ts index cc61135..1fb77aa 100644 --- a/quality/mcp/vendor/apis/MessagingApi.ts +++ b/quality/mcp/vendor/apis/MessagingApi.ts @@ -132,9 +132,9 @@ import { export interface CreateReadMessageRequest { token: string; - messageId?: string; - msgId?: string; - typingIndicator?: boolean; + messageId?: (string) | undefined; + msgId?: (string) | undefined; + typingIndicator?: (boolean) | undefined; } export interface CreateUploadMediaOperationRequest { @@ -158,14 +158,14 @@ export interface GetMmLiteStatusRequest { export interface ListMessagesRequest { token: string; - chatId?: string; - limit?: number; - last?: boolean; - lastMessageNumber?: number; - firstMessageNumber?: number; - minTime?: number; - maxTime?: number; - msgId?: string; + chatId?: (string) | undefined; + limit?: (number) | undefined; + last?: (boolean) | undefined; + lastMessageNumber?: (number) | undefined; + firstMessageNumber?: (number) | undefined; + minTime?: (number) | undefined; + maxTime?: (number) | undefined; + msgId?: (string) | undefined; } export interface RetrieveMediaRequest { @@ -180,22 +180,22 @@ export interface SendAddressMessageOperationRequest { export interface SendButtonOperationRequest { token: string; - sendButtonRequest?: SendButtonRequest; + sendButtonRequest?: (SendButtonRequest) | undefined; } export interface SendCarouselRequest { token: string; - body?: string; - cards?: Array<{ [key: string]: any; }>; - params?: Array; - quotedMsgId?: string; - chatId?: string; - phone?: number; + body?: (string) | undefined; + cards?: (Array<{ [key: string]: any; }>) | undefined; + params?: (Array) | undefined; + quotedMsgId?: (string) | undefined; + chatId?: (string) | undefined; + phone?: (number) | undefined; } export interface SendContactOperationRequest { token: string; - sendContactRequest?: SendContactRequest; + sendContactRequest?: (SendContactRequest) | undefined; } export interface SendCtaUrlOperationRequest { @@ -205,15 +205,15 @@ export interface SendCtaUrlOperationRequest { export interface SendFileRequest { token: string; - body?: string; - filename?: string; - mediaId?: string; - mediaType?: SendFileMediaTypeEnum; - voice?: boolean; - caption?: string; - quotedMsgId?: string; - chatId?: string; - phone?: number; + body?: (string) | undefined; + filename?: (string) | undefined; + mediaId?: (string) | undefined; + mediaType?: (SendFileMediaTypeEnum) | undefined; + voice?: (boolean) | undefined; + caption?: (string) | undefined; + quotedMsgId?: (string) | undefined; + chatId?: (string) | undefined; + phone?: (number) | undefined; } export interface SendFlowRequest { @@ -222,46 +222,46 @@ export interface SendFlowRequest { flowId: string; flowToken: string; flowCta: string; - header?: SendFlowRequestHeader; - footer?: string; - flowAction?: SendFlowFlowActionEnum; - flowActionPayload?: object; - flowMessageVersion?: string; - mode?: SendFlowModeEnum; - flowActionData?: object; - flowActionScreen?: string; - quotedMsgId?: string; - chatId?: string; - phone?: number; + header?: (SendFlowRequestHeader) | undefined; + footer?: (string) | undefined; + flowAction?: (SendFlowFlowActionEnum) | undefined; + flowActionPayload?: (object) | undefined; + flowMessageVersion?: (string) | undefined; + mode?: (SendFlowModeEnum) | undefined; + flowActionData?: (object) | undefined; + flowActionScreen?: (string) | undefined; + quotedMsgId?: (string) | undefined; + chatId?: (string) | undefined; + phone?: (number) | undefined; } export interface SendListOperationRequest { token: string; - sendListRequest?: SendListRequest; + sendListRequest?: (SendListRequest) | undefined; } export interface SendLocationRequest { token: string; lat: string; lng: string; - address?: string; - name?: string; - quotedMsgId?: string; - chatId?: string; - phone?: number; + address?: (string) | undefined; + name?: (string) | undefined; + quotedMsgId?: (string) | undefined; + chatId?: (string) | undefined; + phone?: (number) | undefined; } export interface SendLocationRequestOperationRequest { token: string; - sendLocationRequestRequest?: SendLocationRequestRequest; + sendLocationRequestRequest?: (SendLocationRequestRequest) | undefined; } export interface SendMessageRequest { token: string; body: string; - quotedMsgId?: string; - chatId?: string; - phone?: number; + quotedMsgId?: (string) | undefined; + chatId?: (string) | undefined; + phone?: (number) | undefined; } export interface SendOrderDetailsOperationRequest { @@ -281,7 +281,7 @@ export interface SendProductOperationRequest { export interface SendReactionOperationRequest { token: string; - sendReactionRequest?: SendReactionRequest; + sendReactionRequest?: (SendReactionRequest) | undefined; } export interface SendStickerOperationRequest { diff --git a/quality/mcp/vendor/apis/TemplatesApi.ts b/quality/mcp/vendor/apis/TemplatesApi.ts index fbe4d95..698890e 100644 --- a/quality/mcp/vendor/apis/TemplatesApi.ts +++ b/quality/mcp/vendor/apis/TemplatesApi.ts @@ -46,14 +46,14 @@ import { export interface AddTemplateOperationRequest { token: string; - addTemplateRequest?: AddTemplateRequest; + addTemplateRequest?: (AddTemplateRequest) | undefined; } export interface ListTemplatesRequest { token: string; - limit?: number; - offset?: number; - sort?: ListTemplatesSortEnum; + limit?: (number) | undefined; + offset?: (number) | undefined; + sort?: (ListTemplatesSortEnum) | undefined; } export interface RemoveTemplateOperationRequest { @@ -63,7 +63,7 @@ export interface RemoveTemplateOperationRequest { export interface SendTemplateOperationRequest { token: string; - sendTemplateRequest?: SendTemplateRequest; + sendTemplateRequest?: (SendTemplateRequest) | undefined; } /** From d91d09a2ace2404845484d06eb9074fd51f95269 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 00:29:54 +0500 Subject: [PATCH 04/11] ci(quality): replay historical complexity with sealed analyzer 1.0.8 --- quality/baselines/complexity.json | 20 +- tools/quality/README.md | 27 ++- tools/quality/canonical-duplicates.mjs | 128 +++++++++++++ tools/quality/complexity.mjs | 83 +++++++++ tools/quality/lib/guards.mjs | 74 +++++--- tools/quality/lib/historical.mjs | 2 +- tools/quality/lib/reports.mjs | 222 ++++++++++++++++++++-- tools/quality/package-lock.json | 4 +- tools/quality/package.json | 2 +- tools/quality/run.mjs | 8 + tools/quality/template-manifest.json | 24 ++- tools/quality/test/cli.test.mjs | 241 ++++++++++++++++++++++++ tools/quality/test/complexity.test.mjs | 247 +++++++++++++++++++++++++ tools/quality/test/ratchet.test.mjs | 28 ++- tools/quality/test/reports.test.mjs | 192 +++++++++++++++++++ 15 files changed, 1236 insertions(+), 66 deletions(-) create mode 100644 tools/quality/canonical-duplicates.mjs create mode 100644 tools/quality/complexity.mjs create mode 100644 tools/quality/test/complexity.test.mjs create mode 100644 tools/quality/test/reports.test.mjs diff --git a/quality/baselines/complexity.json b/quality/baselines/complexity.json index fd67bc4..15c05bd 100644 --- a/quality/baselines/complexity.json +++ b/quality/baselines/complexity.json @@ -3,11 +3,11 @@ "adoptionBase": "b46574a4563dc7cae6a7dfc5b378058c01053aa8", "entries": [ { - "key": "src/http.ts|max-lines-per-function|createHttpApp|0", - "value": 176, + "key": "src/http.ts|max-lines-per-function|[[\"FunctionDeclaration\",\"createHttpApp\",\"instance\"],[\"VariableDeclaration\",\"handleMcp\"],[\"ArrowFunction\",\"\",\"instance\"]]", + "value": 144, "path": "src/http.ts", "rule": "max-lines-per-function", - "scope": "createHttpApp", + "scope": "[[\"FunctionDeclaration\",\"createHttpApp\",\"instance\"],[\"VariableDeclaration\",\"handleMcp\"],[\"ArrowFunction\",\"\",\"instance\"]]", "owner": "1MSG SDK/tooling maintainers", "expires": "2026-12-11", "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", @@ -17,11 +17,11 @@ } }, { - "key": "src/http.ts|max-lines-per-function|handleMcp|0", - "value": 144, + "key": "src/http.ts|max-lines-per-function|[[\"FunctionDeclaration\",\"createHttpApp\",\"instance\"]]", + "value": 176, "path": "src/http.ts", "rule": "max-lines-per-function", - "scope": "handleMcp", + "scope": "[[\"FunctionDeclaration\",\"createHttpApp\",\"instance\"]]", "owner": "1MSG SDK/tooling maintainers", "expires": "2026-12-11", "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", @@ -31,11 +31,11 @@ } }, { - "key": "src/polyfill-channel-settings.ts|max-params||0", + "key": "src/polyfill-channel-settings.ts|max-params|[[\"FunctionDeclaration\",\"polyfillChannelSettings\",\"instance\"],[\"assignment\",[\"member\",[\"identifier\",\"messaging\"],\"sendCarousel\"]],[\"ArrowFunction\",\"\",\"instance\"]]", "value": 6, "path": "src/polyfill-channel-settings.ts", "rule": "max-params", - "scope": "", + "scope": "[[\"FunctionDeclaration\",\"polyfillChannelSettings\",\"instance\"],[\"assignment\",[\"member\",[\"identifier\",\"messaging\"],\"sendCarousel\"]],[\"ArrowFunction\",\"\",\"instance\"]]", "owner": "1MSG SDK/tooling maintainers", "expires": "2026-12-11", "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", @@ -45,11 +45,11 @@ } }, { - "key": "src/polyfill-channel-settings.ts|max-params|sendCarousel|0", + "key": "src/polyfill-channel-settings.ts|max-params|[[\"FunctionDeclaration\",\"polyfillChannelSettings\",\"instance\"]]", "value": 6, "path": "src/polyfill-channel-settings.ts", "rule": "max-params", - "scope": "sendCarousel", + "scope": "[[\"FunctionDeclaration\",\"polyfillChannelSettings\",\"instance\"]]", "owner": "1MSG SDK/tooling maintainers", "expires": "2026-12-11", "reason": "Historical maintainability findings replayed at the adoption base; individual measurements cannot increase.", diff --git a/tools/quality/README.md b/tools/quality/README.md index ebb38d7..4b600bd 100644 --- a/tools/quality/README.md +++ b/tools/quality/README.md @@ -1,4 +1,4 @@ -# 1MSG quality template 1.0.2 +# 1MSG quality template 1.0.8 Vendored, private template for 1MSG-156. Install its exact tools with `npm --prefix tools/quality ci` on Node 24.19.0. The lockfile and @@ -83,3 +83,28 @@ Integrity verification prevents hiding authored code under the vendor exemption. No analyzer flag may depend on CI or a skip-type-aware environment variable. Version 1.0.2 resolves Svelte embedded clone paths to their physical source while retaining fragment identity. Initial Knip replay omits only the exact current-only sealed vendor ignore; authored stale ignores remain errors. + +## Canonical maintainability measurement (1.0.3) + +For adoption that includes formatting, run `node tools/quality/complexity.mjs --config eslint.complexity.config.mjs` as the analyzer inside the existing `report --kind eslint --name complexity` gate. Both trusted historical source and current source are independently formatted in memory with the pinned Prettier version and sealed policy before the same ESLint rules execute. Files on disk are never rewritten. This gives line metrics the same unit of measurement; AST complexity and the per-finding ratchet remain enforced. The original Git source hash remains adoption evidence. The JSON report includes canonical source so diagnostic locations identify the correct function after formatting. Formatter/parse failures are fatal. This does not replace `Prettier --check` on the actual checkout. + +Existing consumers pinned to 1.0.2 do not need a policy change; 1.0.3 adds the explicit canonical measurement command. + +For JS/TS formatting migrations, `duplicateCanonicalFormat: true` measures both trusted base and current source after the same pinned formatter in disposable copies. Declare the supported `duplicateFormats` (`javascript`, `typescript`, `jsx`, `tsx`) and exact ignored dependency/generated paths. Source files stay unchanged. New/grown clone groups still fail even if the aggregate percentage falls. Unsupported framework source, unexpected symlinks, empty coverage or failed formatting are analyzer failures. Report line numbers refer to canonical source fragments. + +Version 1.0.6 runs canonical complexity with inline ESLint configuration disabled on both sides. Legacy directives for unrelated framework plugins cannot corrupt metric coverage, and a source comment cannot suppress complexity debt. The separate source suppression guard still enforces the current-code policy. Trusted replay also copies exact Jest config filenames as analyzer inputs, allowing Knip to load current test-runner configuration against historical application and test source. Ordinary source files continue to come exclusively from the trusted Git archive. + +Version 1.0.7 parses JS/TS/JSX/TSX with the pinned TypeScript compiler and prints an in-memory, comment-free metric input before pinned Prettier formatting. Comments inside JSX callbacks otherwise change indentation and wrap executable lines, inflating line debt despite `skipComments`. Both current and trusted historical sources use the same normalization; recompute both sides when adopting this version. No rule limits or baseline allowances change. The original files are not rewritten, and strings, regular expressions, template literals and rendered JSX content remain data rather than comment matches. Unsupported source extensions and syntax errors fail closed before recovered syntax can become a metric report. + +The 31-test suite includes actual CLI fixtures proving that JSX documentation leaves file/function line metrics unchanged, an added statement still increases those metrics, malformed JS/TSX fails, and canonical string/tagged-template/regular-expression, JSX rendering and automatic-semicolon behavior is preserved. JSX assertions compare rendered text and element order because formatting may split adjacent text nodes. Formatting and documentation guards still inspect the actual checkout. Consumers remaining on sealed 1.0.2 are unchanged by this release. + +## Version 1.0.8 + +Complexity findings use complete lexical ownership (class, function, binding, +property and call argument), independent of emitted diagnostic order. Fixing an +older callback therefore cannot renumber a different callback's allowance. +Ambiguous sibling scopes are tied to ordered, comment-free AST evidence; changed +or indistinguishable groups fail closed. The real CLI fixtures verify both +legitimate debt reduction and rejection of transferred/increased allowances. +Existing consumers must replay historical source with this version when adopting +the new key schema; old diagnostic ordinals are never silently migrated. diff --git a/tools/quality/canonical-duplicates.mjs b/tools/quality/canonical-duplicates.mjs new file mode 100644 index 0000000..c706926 --- /dev/null +++ b/tools/quality/canonical-duplicates.mjs @@ -0,0 +1,128 @@ +import { spawnSync } from 'node:child_process'; +import { + lstat, + mkdir, + mkdtemp, + readFile, + readdir, + rm, + writeFile, + realpath, +} from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { format } from 'prettier'; +import picomatch from 'picomatch'; +import policy from './prettier.config.mjs'; +const config = JSON.parse(process.argv[2]); +const root = process.cwd(); +if (!Array.isArray(config.duplicatePaths) || !config.duplicatePaths.length) + throw new Error('Canonical clone roots are required'); +for (const file of config.duplicatePaths) + if ( + typeof file !== 'string' || + file.startsWith('/') || + file.split(/[\\/]/).includes('..') + ) + throw new Error('Clone root must remain inside the repository'); +const formats = config.duplicateFormats ?? [ + 'javascript', + 'typescript', + 'jsx', + 'tsx', +]; +if ( + !formats.length || + formats.some( + (value) => !['javascript', 'typescript', 'jsx', 'tsx'].includes(value), + ) +) + throw new Error( + 'Canonical clone measurement supports only declared JavaScript and TypeScript formats', + ); +const temporary = await realpath( + await mkdtemp(join(tmpdir(), 'canonical-clones-')), +); +const ignored = picomatch(config.duplicateIgnore ?? [], { dot: true }); +let files = 0; +/** Inventory declared roots without following symlinks and format each authored source with the same sealed policy. */ +async function copy(file) { + if (ignored(file)) return; + const info = await lstat(join(root, file)); + if (info.isSymbolicLink()) throw Error(`Unreviewed source symlink: ${file}`); + if (info.isDirectory()) { + for (const entry of await readdir(join(root, file))) + await copy(join(file, entry)); + return; + } + if (/\.(svelte|vue)$/.test(file)) + throw new Error( + `Canonical clone measurement needs a framework formatter for ${file}`, + ); + if (!/\.[cm]?[jt]sx?$/.test(file)) return; + const canonical = await format(await readFile(join(root, file), 'utf8'), { + ...policy, + filepath: file, + }); + const target = join(temporary, file); + await mkdir(dirname(target), { recursive: true }); + await writeFile(target, canonical); + files++; +} +/** Restore portable source identities after analyzing the temporary canonical copy; fragments retain their measured text. */ +function originalPaths(value) { + if (typeof value === 'string' && value.startsWith(temporary + '/')) + return join(root, value.slice(temporary.length + 1)); + if (Array.isArray(value)) return value.map(originalPaths); + if (value && typeof value === 'object') + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [ + originalPaths(key), + originalPaths(item), + ]), + ); + return value; +} +try { + for (const file of config.duplicatePaths) await copy(file); + if (!files) throw Error('Canonical clone inventory is empty'); + const tool = new URL('./node_modules/jscpd/run-jscpd.js', import.meta.url) + .pathname; + const result = spawnSync( + process.execPath, + [ + tool, + ...config.duplicatePaths, + '--reporters', + 'json', + '--output', + 'reports', + '--mode', + 'mild', + '--min-lines', + '5', + '--min-tokens', + '50', + '--silent', + '--absolute', + '--format', + formats.join(','), + ], + { + cwd: temporary, + encoding: 'utf8', + timeout: 180000, + maxBuffer: 50 * 1024 * 1024, + }, + ); + if (result.status !== 0 || result.error || result.signal) + throw Error(result.stderr || result.stdout || String(result.error)); + const report = JSON.parse( + await readFile(join(temporary, 'reports/jscpd-report.json'), 'utf8'), + ); + if (report.duplicates.some((clone) => typeof clone.fragment !== 'string')) + throw Error('Missing canonical clone fragment'); + process.stdout.write(JSON.stringify(originalPaths(report))); +} finally { + await rm(temporary, { recursive: true, force: true }); +} diff --git a/tools/quality/complexity.mjs b/tools/quality/complexity.mjs new file mode 100644 index 0000000..fb0b27a --- /dev/null +++ b/tools/quality/complexity.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +import { realpathSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { extname } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { ESLint } from 'eslint'; +import { format } from 'prettier'; +import ts from 'typescript'; + +import policy from './prettier.config.mjs'; + +/** Remove only parsed comments before formatting so documentation cannot inflate wrapped code lines; reject unsupported or invalid syntax. */ +function metricSource(source, filePath) { + if (!/^[.](?:[cm]?[jt]s|[jt]sx)$/.test(extname(filePath))) + throw new Error(`Unsupported complexity source: ${filePath}`); + const syntax = ts.createSourceFile( + filePath, + source, + ts.ScriptTarget.Latest, + true, + ); + if (syntax.parseDiagnostics.length) { + const diagnostic = syntax.parseDiagnostics[0]; + throw new Error( + `Complexity parse failed: ${filePath}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`, + ); + } + return ts + .createPrinter({ removeComments: true, newLine: ts.NewLineKind.LineFeed }) + .printFile(syntax); +} + +/** Measure comment-free source with the same pinned formatting policy, retaining canonical text for diagnostic scope identities. */ +export async function measureComplexity(config, targets) { + const eslint = new ESLint({ + overrideConfigFile: config, + allowInlineConfig: false, + }); + const inventory = await eslint.lintFiles(targets); + if (!inventory.length) + throw new Error('Complexity source inventory is empty'); + const reports = []; + for (const file of inventory) { + if (file.fatalErrorCount) + throw new Error(`Complexity parse failed: ${file.filePath}`); + const source = await readFile(file.filePath, 'utf8'); + const canonical = await format(metricSource(source, file.filePath), { + ...policy, + filepath: file.filePath, + }); + const [report] = await eslint.lintText(canonical, { + filePath: file.filePath, + }); + if (!report || report.fatalErrorCount) + throw new Error(`Complexity parse failed: ${file.filePath}`); + reports.push({ ...report, source: canonical }); + } + return reports; +} + +/** Keep formatter, parser and inventory failures distinct from ordinary maintainability diagnostics. */ +async function main() { + const args = process.argv.slice(2); + const position = args.indexOf('--config'); + if (position < 0 || !args[position + 1]) + throw new Error('Complexity measurement requires --config'); + const [config] = args.splice(position, 2).slice(1); + const reports = await measureComplexity(config, args.length ? args : ['.']); + process.stdout.write(JSON.stringify(reports)); + if (reports.some((report) => report.errorCount || report.warningCount)) + process.exitCode = 1; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href +) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 2; + }); +} diff --git a/tools/quality/lib/guards.mjs b/tools/quality/lib/guards.mjs index 500c8fe..f27c583 100644 --- a/tools/quality/lib/guards.mjs +++ b/tools/quality/lib/guards.mjs @@ -30,6 +30,11 @@ export function validateConfig( 'sourceRoots must inventory the authored source and tooling', ); } + if ( + config.duplicateCanonicalFormat !== undefined && + typeof config.duplicateCanonicalFormat !== 'boolean' + ) + throw new Error('duplicateCanonicalFormat must be an explicit boolean'); for (const root of config.sourceRoots) { if (!existsSync(root)) throw new Error(`Missing source root: ${root}`); } @@ -93,28 +98,32 @@ export function parseUnits(path, text) { ]; } -/** Compare semantic tokens so formatting/quote changes do not create a false method edit. */ -function functionFingerprint(fragment) { - const scanner = ts.createScanner( - ts.ScriptTarget.Latest, - true, - ts.LanguageVariant.JSX, - fragment, - ); +/** Compare parsed semantic tokens, including template segments, without treating comments or optional trailing commas as method edits. */ +function functionFingerprint(node, ast) { const tokens = []; - for ( - let token = scanner.scan(); - token !== ts.SyntaxKind.EndOfFileToken; - token = scanner.scan() - ) { - const value = [ - ts.SyntaxKind.StringLiteral, - ts.SyntaxKind.NoSubstitutionTemplateLiteral, - ].includes(token) - ? scanner.getTokenValue() - : scanner.getTokenText(); - tokens.push([token, value]); + /** Traverse lexical tokens rather than rescanning text, so template-expression context remains accurate. */ + function visit(current) { + if ( + current.kind >= ts.SyntaxKind.FirstJSDocNode && + current.kind <= ts.SyntaxKind.LastJSDocNode + ) + return; + const children = current.getChildren(ast); + if (children.length) { + for (const child of children) visit(child); + return; + } + if (current.kind === ts.SyntaxKind.SyntaxList) return; + const value = + ts.isStringLiteralLike(current) || + ts.isTemplateHead(current) || + ts.isTemplateMiddle(current) || + ts.isTemplateTail(current) + ? current.text + : current.getText(ast); + tokens.push([current.kind, value]); } + visit(node); const meaningful = tokens.filter(([kind], index) => { if (kind !== ts.SyntaxKind.CommaToken) return true; const next = tokens[index + 1]?.[0]; @@ -134,6 +143,27 @@ function functionFingerprint(fragment) { return digest(JSON.stringify(meaningful)); } +/** Identify a call chain without embedding mutable arguments or comments into callback ownership. */ +function lexicalCall(node, ast) { + if ( + ts.isParenthesizedExpression(node) || + ts.isAsExpression(node) || + ts.isNonNullExpression(node) + ) + return lexicalCall(node.expression, ast); + if (ts.isCallExpression(node) || ts.isNewExpression(node)) + return `${lexicalCall(node.expression, ast)}()`; + if (ts.isPropertyAccessExpression(node)) + return `${lexicalCall(node.expression, ast)}.${node.name.text}`; + if (ts.isElementAccessExpression(node)) { + const key = ts.isStringLiteralLike(node.argumentExpression) + ? node.argumentExpression.text + : ''; + return `${lexicalCall(node.expression, ast)}[${key}]`; + } + return ts.isIdentifier(node) ? node.text : ''; +} + /** Identify callbacks by their lexical owner, not their position among unrelated functions. */ function lexicalScope(node, ast) { const owners = []; @@ -142,7 +172,7 @@ function lexicalScope(node, ast) { const name = current.name ?? current.parent?.name; if (name) owners.push(name.getText(ast)); } else if (ts.isCallExpression(current)) { - owners.push(current.expression.getText(ast)); + owners.push(lexicalCall(current.expression, ast)); } } return owners.reverse().join('/'); @@ -212,7 +242,7 @@ export function functionsIn(path, text) { : []; functions.push({ key: `${suffix}:${lexicalScope(node, ast)}:${name}`, - signature: functionFingerprint(node.getText(ast)), + signature: functionFingerprint(node, ast), name, comments: [ ...comments, diff --git a/tools/quality/lib/historical.mjs b/tools/quality/lib/historical.mjs index 7ed6be8..29ba0a2 100644 --- a/tools/quality/lib/historical.mjs +++ b/tools/quality/lib/historical.mjs @@ -94,7 +94,7 @@ export function historicalAnalysis(base, config, templateDirectory, analyze) { cpSync('package.json', join(snapshot, 'package.json')); } const configuration = paths.filter((path) => - /(?:^|\/)(?:eslint(?:\.[\w-]+)?\.config\.[cm]?js|dependency-cruiser(?:\.[\w-]+)?\.config\.[cm]?js|tsconfig(?:\.[\w-]+)?\.json|quality\.config\.mjs)$/.test( + /(?:^|\/)(?:eslint(?:\.[\w-]+)?\.config\.[cm]?js|dependency-cruiser(?:\.[\w-]+)?\.config\.[cm]?js|jest(?:\.[\w-]+)?\.config\.[cm]?[jt]s|tsconfig(?:\.[\w-]+)?\.json|quality\.config\.mjs)$/.test( path, ), ); diff --git a/tools/quality/lib/reports.mjs b/tools/quality/lib/reports.mjs index 19c9a8e..b04abef 100644 --- a/tools/quality/lib/reports.mjs +++ b/tools/quality/lib/reports.mjs @@ -184,25 +184,200 @@ export function duplicateRecords(report) { .sort((a, b) => a.key.localeCompare(b.key)); } -/** Locate the enclosing declaration for an ESLint diagnostic without using unstable line numbers as identity. */ -function scopeAt(source, line, column) { +/** Fingerprint parsed syntax without comments, retaining literal and template text that can change callback behavior. */ +function syntaxFingerprint(node, source) { + const tokens = []; + /** Preserve parser token boundaries, including template substitutions, instead of rescanning a context-free fragment. */ + function visit(current) { + if ( + current.kind >= ts.SyntaxKind.FirstJSDocNode && + current.kind <= ts.SyntaxKind.LastJSDocNode + ) + return; + const children = current.getChildren(source); + if (children.length) { + for (const child of children) visit(child); + } else if (current.kind !== ts.SyntaxKind.SyntaxList) { + tokens.push([current.kind, current.getText(source)]); + } + } + visit(node); + return digest(JSON.stringify(tokens)); +} + +/** Identify a callable expression without incorporating the bodies or mutable arguments of other callbacks in its chain. */ +function callTarget(node, source) { + if ( + ts.isParenthesizedExpression(node) || + ts.isAsExpression(node) || + ts.isTypeAssertionExpression(node) || + ts.isNonNullExpression(node) || + ts.isSatisfiesExpression(node) + ) + return callTarget(node.expression, source); + if (ts.isIdentifier(node)) return ['identifier', node.text]; + if (node.kind === ts.SyntaxKind.ThisKeyword) return ['this']; + if (node.kind === ts.SyntaxKind.SuperKeyword) return ['super']; + if (ts.isPropertyAccessExpression(node)) + return ['member', callTarget(node.expression, source), node.name.text]; + if (ts.isElementAccessExpression(node)) + return [ + 'element', + callTarget(node.expression, source), + syntaxFingerprint(node.argumentExpression, source), + ]; + if (ts.isCallExpression(node) || ts.isNewExpression(node)) + return [ts.SyntaxKind[node.kind], callTarget(node.expression, source)]; + if (ts.isFunctionLike(node)) + return [ + ts.SyntaxKind[node.kind], + node.name?.getText(source) ?? '', + ]; + return ['expression', syntaxFingerprint(node, source)]; +} + +/** Keep ordinary binding names readable while preserving computed and destructured names without formatting-sensitive text. */ +function ownerName(node, source) { + if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text; + if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) + return ['literal', node.text]; + return ['computed', syntaxFingerprint(node, source)]; +} + +/** Include every named lexical owner and each enclosing call's argument position, without using source lines or diagnostic order. */ +function lexicalOwners(node, source) { + const owners = []; + let child; + for ( + let current = node; + current && !ts.isSourceFile(current); + current = current.parent + ) { + if (ts.isCallExpression(current) || ts.isNewExpression(current)) { + const argument = current.arguments?.indexOf(child) ?? -1; + owners.push([ + ts.SyntaxKind[current.kind], + callTarget(current.expression, source), + argument < 0 ? 'callee' : argument, + ]); + } else if ( + ts.isBinaryExpression(current) && + current.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + current.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) { + owners.push(['assignment', callTarget(current.left, source)]); + } else if (ts.isFunctionLike(current) || ts.isClassLike(current)) { + owners.push([ + ts.SyntaxKind[current.kind], + current.name ? ownerName(current.name, source) : '', + current.modifiers?.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ) + ? 'static' + : 'instance', + ]); + } else if (current.name) { + owners.push([ + ts.SyntaxKind[current.kind], + ownerName(current.name, source), + ]); + } else if ( + ts.isJsxOpeningElement(current) || + ts.isJsxSelfClosingElement(current) + ) { + owners.push(['jsx', current.tagName.getText(source)]); + } + child = current; + } + return owners.reverse(); +} + +/** Recognize independently reported nesting nodes; sibling blocks must not share a function's depth allowance. */ +function isDepthScope(node) { + return [ + ts.SyntaxKind.IfStatement, + ts.SyntaxKind.ForStatement, + ts.SyntaxKind.ForInStatement, + ts.SyntaxKind.ForOfStatement, + ts.SyntaxKind.WhileStatement, + ts.SyntaxKind.DoStatement, + ts.SyntaxKind.SwitchStatement, + ts.SyntaxKind.TryStatement, + ts.SyntaxKind.WithStatement, + ].includes(node.kind); +} + +/** Index all lexical scopes, including functions below lint thresholds, so disappearing diagnostics cannot renumber another allowance. */ +function scopeIdentities(source, depth = false) { + const groups = new Map(); + /** Inventory complete syntax before resolving any diagnostic, keeping below-threshold siblings in ambiguity decisions. */ + function visit(node) { + const included = depth + ? isDepthScope(node) + : (ts.isFunctionLike(node) && node.body) || + ts.isClassStaticBlockDeclaration(node) || + (ts.isPropertyDeclaration(node) && node.initializer); + if (included) { + const owners = lexicalOwners(node, source); + if (depth) owners.push(['nesting', ts.SyntaxKind[node.kind]]); + if (ts.isClassStaticBlockDeclaration(node)) owners.push(['static-block']); + const owner = JSON.stringify(owners); + const group = groups.get(owner) ?? []; + group.push(node); + groups.set(owner, group); + } + ts.forEachChild(node, visit); + } + visit(source); + const identities = new Map(); + for (const [owner, nodes] of groups) { + if (nodes.length === 1) { + identities.set(nodes[0], owner); + continue; + } + const fingerprints = nodes.map((node) => syntaxFingerprint(node, source)); + // Preserve source order in ambiguous groups: swapping a large and small body must not transfer the large allowance. + const group = digest(JSON.stringify(fingerprints)); + for (const [index, node] of nodes.entries()) { + const fingerprint = fingerprints[index]; + // A changed ambiguous group loses every old allowance; even matching another old body cannot borrow its budget. + identities.set( + node, + fingerprints.indexOf(fingerprint) === + fingerprints.lastIndexOf(fingerprint) + ? `${owner}|group:${group}|member:${fingerprint}` + : null, + ); + } + } + return identities; +} + +/** Resolve a diagnostic to its innermost inventoried owner; indistinguishable siblings and missing locations fail closed. */ +function scopeAt(source, identities, line, column) { const offset = source.getPositionOfLineAndCharacter( Math.max(0, line - 1), Math.max(0, column - 1), ); - let scope = ''; - const visit = (node) => { - if (node.pos <= offset && node.end >= offset) { - if (ts.isFunctionLike(node)) - scope = - node.name?.getText(source) ?? - node.parent.name?.getText(source) ?? - ''; + let matched; + /** Select the innermost scope containing the reported token, never an unrelated neighboring declaration. */ + function visit(node) { + if (node.getStart(source) <= offset && node.end > offset) { + if (identities.has(node)) matched = node; ts.forEachChild(node, visit); } - }; + } visit(source); - return scope; + if (!matched) + throw new Error( + `No lexical scope for ESLint diagnostic: ${source.fileName}:${line}:${column}`, + ); + const identity = identities.get(matched); + if (identity === null) + throw new Error( + `Ambiguous lexical scope for ESLint diagnostic: ${source.fileName}:${line}:${column}`, + ); + return identity; } /** Only complexity diagnostics may use a debt baseline; parser, typed and correctness errors remain fatal. */ @@ -221,13 +396,18 @@ export function eslintRecords(report) { for (const file of report) { const path = repoPath(file.filePath); if (!Array.isArray(file.messages)) throw new Error('Invalid ESLint report'); + if (file.messages.length === 0) continue; const source = ts.createSourceFile( path, - currentFile(path) ?? '', + file.source ?? currentFile(path) ?? '', ts.ScriptTarget.Latest, true, ); - const occurrences = new Map(); + if (source.parseDiagnostics.length) + throw new Error(`Invalid syntax in ESLint scope evidence: ${path}`); + const functions = scopeIdentities(source); + const depths = scopeIdentities(source, true); + const identities = new Set(); for (const message of file.messages) { if (message.fatal || !allowed.has(message.ruleId)) { throw new Error( @@ -248,12 +428,18 @@ export function eslintRecords(report) { const scope = message.ruleId === 'max-lines' ? '' - : scopeAt(source, message.line ?? 1, message.column ?? 1); + : scopeAt( + source, + message.ruleId === 'max-depth' ? depths : functions, + message.line ?? 1, + message.column ?? 1, + ); const identity = `${path}|${message.ruleId}|${scope}`; - const occurrence = occurrences.get(identity) ?? 0; - occurrences.set(identity, occurrence + 1); + if (identities.has(identity)) + throw new Error(`Duplicate ESLint diagnostic identity: ${identity}`); + identities.add(identity); results.push( - record(`${identity}|${occurrence}`, Number(values[1]), { + record(identity, Number(values[1]), { path, rule: message.ruleId, scope, diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json index 5b96b4b..8ad5519 100644 --- a/tools/quality/package-lock.json +++ b/tools/quality/package-lock.json @@ -1,12 +1,12 @@ { "name": "@1msg/quality-template", - "version": "1.0.2", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@1msg/quality-template", - "version": "1.0.2", + "version": "1.0.8", "dependencies": { "@eslint/js": "9.39.5", "dependency-cruiser": "18.6.0", diff --git a/tools/quality/package.json b/tools/quality/package.json index 62d20c2..b71d5b0 100644 --- a/tools/quality/package.json +++ b/tools/quality/package.json @@ -1,6 +1,6 @@ { "name": "@1msg/quality-template", - "version": "1.0.2", + "version": "1.0.8", "private": true, "type": "module", "engines": { diff --git a/tools/quality/run.mjs b/tools/quality/run.mjs index e0b21ae..17e3c74 100644 --- a/tools/quality/run.mjs +++ b/tools/quality/run.mjs @@ -69,6 +69,14 @@ function unused(config, production) { function duplicates(config) { if (!config.duplicatePaths?.length) throw new Error('duplicatePaths must list authored runtime/tooling roots'); + if (config.duplicateCanonicalFormat) { + const { report } = analyze( + process.execPath, + [join(directory, 'canonical-duplicates.mjs'), JSON.stringify(config)], + 'Canonical jscpd', + ); + return { findings: duplicateRecords(report), report }; + } const output = mkdtempSync(join(tmpdir(), '1msg-jscpd-')); try { const argv = [ diff --git a/tools/quality/template-manifest.json b/tools/quality/template-manifest.json index 444e17d..da93308 100644 --- a/tools/quality/template-manifest.json +++ b/tools/quality/template-manifest.json @@ -1,23 +1,27 @@ { "schemaVersion": 1, - "version": "1.0.2", + "version": "1.0.8", "files": { - "README.md": "1d3f6f9d420adea402d92aff483c14271e6e24c291437d6ea5b59c3162702a20", + "README.md": "e74940b5acec1bdbff75eb751432967630ddb4215af24d89d31dd3694916c12e", + "canonical-duplicates.mjs": "a3583fdc3e344af448ea8d2543c2398daaa03fa10a0739da486afb04e35425ba", + "complexity.mjs": "4ed5553c392621e0615a48c12e12300c8602ed2a28cfbaff67002acf82e49cac", "eslint.mjs": "607ddfaac3d365fedd7dbc5e21c781d448347b1c36662f3492e4364ff9f22ded", "lib/baseline.mjs": "7876ba90b80448d9c9003c21e348095ed22630c41fc26ed944984742020549dd", "lib/git.mjs": "b7c26871526d864bf8d12f01c613d3bfa0adb2da9d072544d8f09407ef598758", - "lib/guards.mjs": "310c224c5d072fa16b1cf5f0c47e5da4258f0f63fa9a1c4dc114c9fe192bc398", - "lib/historical.mjs": "d338d88bae6d1da19f27296fbb6c57cff4ca09664fe7baa8d6c8d801ca3e69f2", + "lib/guards.mjs": "e94dc6b58703b44420f8b815ee069b9e0a7fa9aefd5faba63d55f1c3da7c4286", + "lib/historical.mjs": "43b46395828daa3f19d8692e9bd22ea6cfc1d0dbac83814d727894f672a0ddd2", "lib/knip-reporter.mjs": "175d7d96e2913e8f6de04b6c706172258d9d7654a26ba7f9ed7edff79d3fc8ca", - "lib/reports.mjs": "f47f6cbf8022691e61a245bf54a5fdd1ba9f2037906ec6576f2ac771224161dc", + "lib/reports.mjs": "60e78d58256d1060067e939397c8760384f202625c394f73d18e3311ccabb44c", "lib/template.mjs": "c2a14b715f0c9dbe8b8243b142f4c6628967400fb8f92767537169e1b7bfd8f4", - "package-lock.json": "931d28ffff6590892a072569df9c57345d0a388a22598b3482dee53136a52828", - "package.json": "b2c70073c423135e4b5abba9507d58fd5136a676339e816b72d6dd4bcbb1b757", + "package-lock.json": "fc4279a0f1393109958815b6b92fc95acd1c5a98387ea6d3f13a61a9aeba8a60", + "package.json": "3e9ef4c922c64deddd6c2c50350def2bf79cd0354af891e5d7051c082c4d4937", "prettier.config.mjs": "7576563a9a30aff8eb3177f9cc2e4bc7a0f6b825042eadb9a619b86f519c5e5e", - "run.mjs": "b60c0fc2419c8cfd32eddf369da38b2d87e2b7eacbc31fda31c598930900f199", + "run.mjs": "7cf26128cdf63f3a7ccab7341a0a274a352fd9bda0521ba41e37216bb9cc249f", "seal.mjs": "fb38c8e08ea3cf9f4dcd95e45ffc931aea3b7a54181fed88d7379f8aad4c050d", - "test/cli.test.mjs": "f743a18f0763cf5b5f42f8d2a4b71f67a4de900a3955a299309395af95b99cf3", - "test/ratchet.test.mjs": "62fef1cc7a1851db81c30966909968ff84637b8ed8003821120269c4fae70d49", + "test/cli.test.mjs": "64dda5d60ef9fd2d444f96b2623b97f6197917f1e121aaf73cbe1dfeff90a8ef", + "test/complexity.test.mjs": "ba1f702ba94de3fcad93cb5e43a49bc2456be1426c81754149420d210f2b3952", + "test/ratchet.test.mjs": "324a932c5a52d1b037e111042e1b41e5686f8d4dbfd0307faf86cee649ebc27a", + "test/reports.test.mjs": "d75a93ee4dc606d9930c3a1e19912e093e8635910a7c21f81c735e209c03835f", "tsconfig.next.json": "d900a35fcb0852376b06854df5be2f8f178e26cb5383b93b07ebeb6220e30faf", "tsconfig.node.json": "627661abcb7e992c4bfaf98d6e68a12678573b5d4f6e821ea0fa43fa6e233f28", "tsconfig.svelte.json": "5777247ea98aaf2d886b87cd12206a641377edcdd086ce5fd5b58adc6f0bc771" diff --git a/tools/quality/test/cli.test.mjs b/tools/quality/test/cli.test.mjs index 897d9cd..624b8ec 100644 --- a/tools/quality/test/cli.test.mjs +++ b/tools/quality/test/cli.test.mjs @@ -508,3 +508,244 @@ test('historical Knip removes a current-only exact vendor ignore without relaxin f.close(); } }); + +/** Pinned formatting defines identical clone units for old and current source, while a new participant still fails the real gate. */ +test('canonical jscpd CLI preserves formatting-only debt and rejects a new clone', () => { + const f = fixture(); + try { + f.write( + 'quality.config.mjs', + `export default { schemaVersion: 1, profile: 'A', defaultBranch: 'main', sourceRoots: ['src'], duplicatePaths: ['src'], duplicateCanonicalFormat: true, duplicateFormats: ['javascript', 'typescript', 'jsx', 'tsx'], duplicateIgnore: [], duplicateMaxPercentage: 100 };\n`, + ); + const source = + 'export function calculate() { const label = "ready"; let value = 0; ' + + Array.from({ length: 20 }, (_, i) => `value += ${i};`).join('') + + 'return { label, value }; }'; + f.write('src/a.js', source); + f.write('src/b.js', source); + const base = f.commit(); + const initial = f.run( + 'duplicates', + '--base', + base, + '--bootstrap', + '--owner', + 'quality-fixture', + '--expires', + '2099-12-31', + '--reason', + 'Historical clone fixture', + '--issue', + 'https://example.invalid/quality', + ); + assert.equal(initial.status, 0, initial.stderr); + const baseline = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/duplicates.json'), 'utf8'), + ); + assert.ok(baseline.entries.length > 0); + f.write( + 'src/a.js', + source.replaceAll(';', ';\n').replaceAll('"ready"', "'ready'"), + ); + const formatted = f.run('duplicates', '--base', base); + assert.equal(formatted.status, 0, formatted.stderr); + f.write('src/c.js', source); + const growth = f.run('duplicates', '--base', base); + assert.notEqual(growth.status, 0); + assert.match(growth.stderr, /New or increased|new\/increased/); + } finally { + f.close(); + } +}); + +/** Invalid syntax or a framework file without its formatter must fail canonical analysis instead of disappearing from coverage. */ +test('canonical jscpd fails closed for unsupported or malformed source', () => { + const f = fixture(); + try { + f.write( + 'quality.config.mjs', + `export default { schemaVersion: 1, profile: 'A', defaultBranch: 'main', sourceRoots: ['src'], duplicatePaths: ['src'], duplicateCanonicalFormat: true, duplicateIgnore: [], duplicateMaxPercentage: 100 };\n`, + ); + f.write('src/index.js', 'export const value = 1;'); + const base = f.commit(); + f.write('src/index.js', 'export const value = ;'); + assert.notEqual(f.run('duplicates', '--base', base).status, 0); + f.write('src/index.js', 'export const value = 1;'); + f.write( + 'src/Widget.svelte', + '

{value}

', + ); + const unsupported = f.run('duplicates', '--base', base); + assert.notEqual(unsupported.status, 0); + assert.match(unsupported.stderr, /framework formatter/); + } finally { + f.close(); + } +}); + +/** Current test-runner config is an analyzer input; historical application and test source must still come only from Git. */ +test('historical replay uses current Jest config without replacing trusted source', () => { + const f = fixture(); + try { + f.write('src/index.js', 'export const value = "historical";\n'); + f.write('jest.config.cjs', 'module.exports = { rootDir: "old" };\n'); + const base = f.commit(); + f.write('src/index.js', 'export const value = "current-only";\n'); + f.write('jest.config.cjs', 'module.exports = { rootDir: __dirname };\n'); + const historical = new URL('../lib/historical.mjs', import.meta.url).href; + const template = fileURLToPath(new URL('../', import.meta.url)); + const script = `import { historicalAnalysis } from ${JSON.stringify(historical)}; + import { readFileSync, existsSync } from 'node:fs'; + const value = historicalAnalysis(${JSON.stringify(base)}, {knipConfig:'knip.json'}, ${JSON.stringify(template)}, () => ({ + config: readFileSync('jest.config.cjs','utf8'), + source: readFileSync('src/index.js','utf8'), + newSource: existsSync('src/current-only.js') + })); process.stdout.write(JSON.stringify(value));`; + f.write( + 'src/current-only.js', + 'throw new Error("must not enter historical source");\n', + ); + const result = spawnSync( + process.execPath, + ['--input-type=module', '-e', script], + { cwd: f.cwd, encoding: 'utf8', timeout: 15000 }, + ); + assert.equal(result.status, 0, result.stderr); + const replay = JSON.parse(result.stdout); + assert.match(replay.config, /__dirname/); + assert.match(replay.source, /historical/); + assert.equal(replay.newSource, false); + } finally { + f.close(); + } +}); + +/** A fixed early callback cannot renumber later debt or lend its larger allowance to a different growing callback. */ +test('actual complexity CLI binds callback allowances to lexical owners', () => { + const f = fixture(); + try { + const preset = new URL('../eslint.mjs', import.meta.url).href; + f.write( + 'eslint.config.mjs', + `import { maintainabilityConfig, tseslint } from '${preset}'; export default [{languageOptions:{parser:tseslint.parser}}, ...maintainabilityConfig(), {rules:{complexity:['error',2]}}];`, + ); + const earlier = + 'const earlier = wrap((value) => { if (value.a) return 1; if (value.b) return 2; if (value.c) return 3; if (value.d) return 4; if (value.e) return 5; return 0; });'; + const later = + 'const later = wrap((value) => { if (value.a) return 1; if (value.b) return 2; return 0; });'; + f.write('src/index.js', `${earlier}\n${later}`); + const base = f.commit(); + const args = [ + 'report', + '--kind', + 'eslint', + '--base', + base, + '--bootstrap', + '--owner', + 'quality-fixture', + '--expires', + '2099-12-31', + '--reason', + 'Historical callback debt', + '--issue', + 'TEST-1', + '--', + process.execPath, + fileURLToPath(new URL('../complexity.mjs', import.meta.url)), + '--config', + 'eslint.config.mjs', + 'src/index.js', + ]; + const original = f.run(...args); + assert.equal(original.status, 0, original.stderr); + const originalBaseline = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.deepEqual( + originalBaseline.entries.map(({ value }) => value).sort(), + [3, 6], + ); + f.write( + 'src/index.js', + `const earlier = wrap((value) => value);\n${later}`, + ); + const fixed = f.run(...args); + assert.equal(fixed.status, 0, fixed.stderr); + const reducedBaseline = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.equal(reducedBaseline.entries.length, 1); + assert.equal( + reducedBaseline.entries[0].key, + originalBaseline.entries.find(({ value }) => value === 3).key, + ); + f.write( + 'src/index.js', + `const earlier = wrap((value) => value);\n${later.replace('return 0;', 'if (value.c) return 3; return 0;')}`, + ); + const growth = f.run(...args); + assert.equal(growth.status, 1, growth.stderr); + assert.match( + growth.stderr, + /Cannot baseline a new\/increased finding.*later/, + ); + } finally { + f.close(); + } +}); + +/** Class field initializers and static blocks are independent ESLint code paths and must keep their class ownership. */ +test('actual complexity CLI resolves class field and static block scopes', () => { + const f = fixture(); + try { + const preset = new URL('../eslint.mjs', import.meta.url).href; + f.write( + 'eslint.config.mjs', + `import { maintainabilityConfig, tseslint } from '${preset}'; export default [{languageOptions:{parser:tseslint.parser}}, ...maintainabilityConfig(), {rules:{complexity:['error',2]}}];`, + ); + f.write( + 'src/index.js', + `class First { + value = ready ? (active ? 1 : 2) : 3; + static { if (ready) start(); if (active) finish(); } + } + class Second { + value = ready ? (active ? 1 : 2) : 3; + static { if (ready) start(); if (active) finish(); } + }`, + ); + const base = f.commit(); + const result = f.run( + 'report', + '--kind', + 'eslint', + '--base', + base, + '--bootstrap', + '--owner', + 'quality-fixture', + '--expires', + '2099-12-31', + '--reason', + 'Historical class initializer debt', + '--issue', + 'TEST-1', + '--', + process.execPath, + fileURLToPath(new URL('../complexity.mjs', import.meta.url)), + '--config', + 'eslint.config.mjs', + 'src/index.js', + ); + assert.equal(result.status, 0, result.stderr); + const baseline = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.equal(baseline.entries.length, 4); + assert.equal(new Set(baseline.entries.map(({ key }) => key)).size, 4); + assert.ok(baseline.entries.every(({ value }) => value === 3)); + } finally { + f.close(); + } +}); diff --git a/tools/quality/test/complexity.test.mjs b/tools/quality/test/complexity.test.mjs new file mode 100644 index 0000000..ccf0730 --- /dev/null +++ b/tools/quality/test/complexity.test.mjs @@ -0,0 +1,247 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { + mkdtempSync, + writeFileSync, + readFileSync, + rmSync, + symlinkSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { runInNewContext } from 'node:vm'; + +import ts from 'typescript'; + +/** Run the shipped CLI against a disposable source file with real maintainability rules. */ +function fixture(t) { + const root = mkdtempSync(join(tmpdir(), 'canonical-complexity-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const preset = new URL('../eslint.mjs', import.meta.url).href; + writeFileSync( + join(root, 'eslint.config.mjs'), + `import { maintainabilityConfig, tseslint } from '${preset}'; export default [{languageOptions:{parser:tseslint.parser}}, ...maintainabilityConfig()];`, + ); + const cli = join(root, 'quality-complexity.mjs'); + symlinkSync( + fileURLToPath(new URL('../complexity.mjs', import.meta.url)), + cli, + ); + /** Preserve analyzer exit status and JSON rather than treating a missing report as an empty finding list. */ + return (source, fileName = 'source.js') => { + writeFileSync(join(root, fileName), source); + const result = spawnSync( + process.execPath, + [cli, '--config', 'eslint.config.mjs', fileName], + { cwd: root, encoding: 'utf8', timeout: 15000 }, + ); + assert.equal(readFileSync(join(root, fileName), 'utf8'), source); + return result; + }; +} + +/** Whitespace-only growth is neutral, while an actual extra statement still increases the measured function. */ +test('canonical CLI uses identical units for historical and current line debt', (t) => { + const run = fixture(t); + const statements = Array.from({ length: 80 }, () => 'value += 1;'); + const compact = `export function count() { let value = 0; ${statements.join('')} return value; }`; + const expanded = `export function count() {\n\nlet value = 0;\n${statements.join('\n\n')}\nreturn value;\n}`; + const original = run(compact), + whitespace = run(expanded); + assert.equal(original.status, 1, original.stderr); + assert.equal(whitespace.status, 1, whitespace.stderr); + const [before] = JSON.parse(original.stdout), + [after] = JSON.parse(whitespace.stdout); + assert.equal( + before.source + .split('\n') + .filter((line) => line.trim()) + .join('\n'), + after.source + .split('\n') + .filter((line) => line.trim()) + .join('\n'), + ); + assert.deepEqual( + before.messages.map(({ ruleId, message }) => ({ ruleId, message })), + after.messages.map(({ ruleId, message }) => ({ ruleId, message })), + ); + const growth = run( + compact.replace('return value;', 'value += 2; return value;'), + ); + assert.equal(growth.status, 1, growth.stderr); + const [increased] = JSON.parse(growth.stdout); + assert.notDeepEqual(increased.messages, before.messages); + assert.ok( + increased.source.split('\n').length > before.source.split('\n').length, + ); +}); + +/** A formatter/parser error is a failed analyzer, never a zero-debt report or a baselinable diagnostic. */ +test('canonical CLI fails closed for invalid syntax', (t) => { + const result = fixture(t)('export function broken( {'); + assert.equal(result.status, 2); + assert.equal(result.stdout, ''); + assert.ok(result.stderr.length > 0); +}); + +/** Source directives cannot hide measured debt, and unrelated historical framework directives cannot become parser diagnostics. */ +test('canonical CLI ignores inline configuration on both metric inputs', (t) => { + const run = fixture(t); + const body = `export function count() { let value = 0; ${Array.from({ length: 80 }, () => 'value += 1;').join('')} return value; }`; + const plain = run(body); + const annotated = run( + '/* eslint-disable unknown-framework/legacy-rule, max-lines-per-function */\n' + + body, + ); + assert.equal(plain.status, 1, plain.stderr); + assert.equal(annotated.status, 1, annotated.stderr); + const select = (result) => + JSON.parse(result.stdout).flatMap((file) => + file.messages.map(({ ruleId, message }) => ({ ruleId, message })), + ); + assert.deepEqual(select(annotated), select(plain)); + assert.ok( + select(annotated).some((item) => item.ruleId === 'max-lines-per-function'), + ); +}); + +/** Execute canonical TS/JSX through the same compiler settings to compare literals, child text and evaluation order. */ +function evaluate(source, fileName) { + const compiled = ts.transpileModule(source, { + fileName, + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.None, + jsx: ts.JsxEmit.React, + jsxFactory: 'element', + }, + }); + const context = {}; + runInNewContext(compiled.outputText, context, { timeout: 1000 }); + return JSON.parse(context.result); +} + +/** Callback documentation and JSX comments cannot change file/function line debt; actual instructions must still grow it. */ +test('canonical CLI strips JSX documentation before measuring wrapped line debt', (t) => { + const run = fixture(t); + const statements = Array.from({ length: 160 }, () => 'value += 1;').join( + '\n', + ); + const declarations = Array.from( + { length: 350 }, + (_, index) => `export const field${index} = ${index};`, + ).join('\n'); + const plain = `export function Card() { + let value = 0; + ${statements} + return
{field.label}
} />; + } + ${declarations}`; + const documented = plain + .replace( + 'export function', + '/** Render the editable panel and preserve its accumulated count. */\nexport function', + ) + .replace( + 'render={', + 'render={/** Provide the field label and accessible description to the panel. */ ', + ) + .replace( + '', + '{/* Keep the visible label inside the field container. */}', + ) + .replace( + 'value += 1;', + 'value += /* Increment the visible count once. */ 1;', + ); + const original = run(plain, 'source.tsx'); + const annotated = run(documented, 'source.tsx'); + assert.equal(original.status, 1, original.stderr); + assert.equal(annotated.status, 1, annotated.stderr); + const [before] = JSON.parse(original.stdout); + const [after] = JSON.parse(annotated.stdout); + assert.equal(after.source, before.source); + assert.deepEqual(after.messages, before.messages); + assert.ok(before.messages.some(({ ruleId }) => ruleId === 'max-lines')); + assert.ok( + before.messages.some(({ ruleId }) => ruleId === 'max-lines-per-function'), + ); + const growth = run( + documented.replace('let value = 0;', 'let value = 0; value += 2;'), + 'source.tsx', + ); + assert.equal(growth.status, 1, growth.stderr); + const [increased] = JSON.parse(growth.stdout); + assert.notDeepEqual( + increased.messages.map(({ ruleId, message }) => ({ ruleId, message })), + before.messages.map(({ ruleId, message }) => ({ ruleId, message })), + ); +}); + +/** Comment-like literals and rendered JSX text/element order survive printing; adjacent text nodes are normalized like DOM content. */ +test('canonical CLI preserves string, tagged-template and JSX semantics', (t) => { + const source = [ + `function element(type, props, ...children) { + const rendered = []; + for (const child of children) { + const previous = rendered.length - 1; + if (typeof child === 'string' && typeof rendered[previous] === 'string') rendered[previous] += child; + else rendered.push(child); + } + return {type, props, children: rendered}; + }`, + 'const value = "https://example.invalid/a/*literal*/";', + 'const raw = String.raw`line\\n/* literal */ ${value} // end`;', + 'const cooked = `first\nsecond ${value}`;', + String.raw`const expression = /\/\*literal\*\//u;`, + 'const node =
leading {/* actual comment */}{value} trailing
;', + 'globalThis.result = JSON.stringify({value, raw, cooked, matches: expression.test(value), node});', + ].join('\n'); + const result = fixture(t)(source, 'source.tsx'); + assert.equal(result.status, 0, result.stderr); + const [report] = JSON.parse(result.stdout); + assert.deepEqual( + evaluate(report.source, 'source.tsx'), + evaluate(source, 'source.tsx'), + ); + assert.equal( + evaluate(report.source, 'source.tsx').node.props.title, + '/* attribute */', + ); + assert.match(evaluate(report.source, 'source.tsx').raw, /line\\n/); +}); + +/** Comments near automatic semicolon insertion and adjacent unary operators cannot change executable statements. */ +test('canonical CLI preserves automatic-semicolon and operator semantics', (t) => { + const source = `function returned() { + return /* Preserve the original newline after return. */ + 42; + } + const sum = 1 + /* Keep the unary plus separate. */ +2; + globalThis.result = JSON.stringify({isUndefined: returned() === undefined, sum});`; + const result = fixture(t)(source); + assert.equal(result.status, 0, result.stderr); + const [report] = JSON.parse(result.stdout); + assert.deepEqual(evaluate(report.source, 'source.js'), { + isUndefined: true, + sum: 3, + }); + assert.deepEqual( + evaluate(report.source, 'source.js'), + evaluate(source, 'source.js'), + ); +}); + +/** Invalid TSX remains an analyzer failure even though the TypeScript printer can emit a recovered syntax tree. */ +test('canonical CLI fails closed before printing a recovered TSX tree', (t) => { + const result = fixture(t)( + 'export const view =
{/* valid comment */}
;', + 'source.tsx', + ); + assert.equal(result.status, 2); + assert.equal(result.stdout, ''); + assert.match(result.stderr, /Complexity parse failed/); +}); diff --git a/tools/quality/test/ratchet.test.mjs b/tools/quality/test/ratchet.test.mjs index 7722924..fc2376c 100644 --- a/tools/quality/test/ratchet.test.mjs +++ b/tools/quality/test/ratchet.test.mjs @@ -193,7 +193,7 @@ test('whole-file line limits keep their identity when the diagnostic moves betwe ], }, ])[0]; - assert.equal(finding(20).key, 'lib/guards.mjs|max-lines||0'); + assert.equal(finding(20).key, 'lib/guards.mjs|max-lines|'); assert.equal(finding(120).key, finding(20).key); }); @@ -236,3 +236,29 @@ test('suppression gates do not mistake strings for directives', () => { 0, ); }); + +/** Formatting within template expressions and a chained call must not invent a new function or callback. */ +test('documentation identities preserve template-expression context and stable call chains', () => { + const before = + 'describe("groupIntoBlocks",()=>{it("splits a parent",()=>{const value=Array.from({length:3},(_,i)=>({key:`p.k${i}`,text:`Text ${i}`}));return value;});});'; + const after = + "describe('groupIntoBlocks', () => {\n it('splits a parent', () => {\n const value = Array.from({length:3}, (_, i) => ({\n key: `p.k${i}`,\n text: `Text ${i}`,\n })); return value;\n });\n});"; + const identity = (source) => + functionsIn('src/a.ts', source).map(({ key, signature }) => ({ + key, + signature, + })); + assert.deepEqual(identity(before), identity(after)); + assert.notDeepEqual( + identity(before), + identity(before.replace('Text ${i}', 'Different ${i}')), + ); + const chained = 'prefetch({ value: 1 }).catch(() => { recover(); });'; + const documented = + 'prefetch({ /** Explain the input. */ value: 1, }).catch(() => { recover(); });'; + assert.deepEqual(identity(chained), identity(documented)); + assert.notDeepEqual( + identity(chained), + identity(chained.replace('prefetch', 'send')), + ); +}); diff --git a/tools/quality/test/reports.test.mjs b/tools/quality/test/reports.test.mjs new file mode 100644 index 0000000..68c39e2 --- /dev/null +++ b/tools/quality/test/reports.test.mjs @@ -0,0 +1,192 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { ESLint } from 'eslint'; + +import { eslintRecords } from '../lib/reports.mjs'; + +/** Generate real small-fixture diagnostics with low thresholds without changing the shipped policy. */ +async function analyze(source, rule = 'complexity', limit = 2) { + const eslint = new ESLint({ + overrideConfigFile: true, + overrideConfig: [ + { + languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } }, + rules: { [rule]: ['error', limit] }, + }, + ], + }); + const reports = await eslint.lintText(source, { filePath: 'fixture.js' }); + assert.equal(reports.length, 1); + assert.equal(reports[0].fatalErrorCount, 0); + return reports.map((report) => ({ + ...report, + filePath: 'fixture.js', + source, + })); +} + +/** Vary callback bodies while retaining their call site to probe allowance transfer rather than report counts. */ +function callback(branches, result = '0') { + return `(value) => { ${Array.from({ length: branches }, (_, index) => `if (value.p${index}) return ${index + 1};`).join(' ')} return ${result}; }`; +} + +/** Resolve the real analyzer report to stable keys, preserving parser and ambiguity failures. */ +async function findings(source, rule, limit) { + return eslintRecords(await analyze(source, rule, limit)); +} + +/** Variables and object/JSX properties name anonymous callbacks even when their containing call is unchanged. */ +test('callback identities retain all named variable and property owners', async () => { + const before = `const first = wrap(${callback(2)}); const second = wrap(${callback(3)}); + const options = { load: wrap(${callback(2)}), save: wrap(${callback(3)}) }; + const view = ;`; + const original = await findings(before); + assert.equal(original.length, 6); + assert.equal(new Set(original.map(({ key }) => key)).size, 6); + const current = await findings(before.replaceAll(callback(2), callback(0))); + assert.equal(current.length, 3); + assert.deepEqual( + current, + original.filter(({ value }) => value === 4), + ); +}); + +/** Method names alone cannot move allowances across class, instance/static, or enclosing function boundaries. */ +test('same method and nested function names keep class and function ownership', async () => { + const before = `class First { handle(value) { if (value.a) return 1; if (value.b) return 2; return 0; } + static handle(value) { if (value.a) return 1; if (value.b) return 2; if (value.c) return 3; return 0; } } + class Second { handle(value) { if (value.a) return 1; if (value.b) return 2; if (value.c) return 3; return 0; } } + function left() { function nested(value) { if (value.a) return 1; if (value.b) return 2; return 0; } return nested; } + function right() { function nested(value) { if (value.a) return 1; if (value.b) return 2; if (value.c) return 3; return 0; } return nested; }`; + const original = await findings(before); + assert.equal(original.length, 5); + const reduced = before.replaceAll( + 'if (value.a) return 1; if (value.b) return 2; return 0;', + 'return value;', + ); + assert.deepEqual( + await findings(reduced), + original.filter(({ value }) => value === 4), + ); +}); + +/** The call target and argument index distinguish callbacks without depending on another diagnostic's presence or order. */ +test('call targets and argument positions distinguish unnamed callbacks', async () => { + const source = `left.accept(${callback(2)}, ${callback(3)}); right.accept(${callback(2)});`; + const report = await analyze(source); + const original = eslintRecords(report); + assert.equal(original.length, 3); + const reordered = report.map((file) => ({ + ...file, + messages: [...file.messages].reverse(), + })); + assert.deepEqual(eslintRecords(reordered), original); + const reduced = source.replace( + `left.accept(${callback(2)},`, + `left.accept(${callback(0)},`, + ); + assert.deepEqual( + await findings(reduced), + original.filter( + ({ key, value }) => !(key.includes('"left"') && value === 3), + ), + ); +}); + +/** Identical caller/name chains require the whole sibling group as evidence, including callbacks currently below the limit. */ +test('ambiguous groups cannot borrow another callback body allowance', async () => { + const before = `run(${callback(5)}); run(${callback(2)});`; + const oldKeys = new Set((await findings(before)).map(({ key }) => key)); + const after = `run(${callback(0)}); run(${callback(5)});`; + const current = await findings(after); + assert.equal(current.length, 1); + assert.ok(current.every(({ key }) => !oldKeys.has(key))); + const belowThreshold = `run(${callback(0)}); run(${callback(2)});`; + const prior = await findings(belowThreshold); + const changedSibling = await findings( + `run(${callback(1)}); run(${callback(2)});`, + ); + assert.notEqual(prior[0].key, changedSibling[0].key); + const originalOrder = await findings( + `run(${callback(5)}); run(${callback(0)});`, + ); + const swappedBodies = await findings( + `run(${callback(0)}); run(${callback(5)});`, + ); + assert.notEqual(originalOrder[0].key, swappedBodies[0].key); +}); + +/** Formatting and comments leave ambiguous syntax fingerprints stable; literal/template/JSX content remains meaningful evidence. */ +test('ambiguous fingerprints preserve source semantics and ignore comments', async () => { + const first = callback(2, '`first ${value.name}`'); + const second = callback(2, 'second'); + const source = `run(${first}); run(${second});`; + const original = await findings(source); + assert.equal(original.length, 2); + assert.deepEqual( + await findings(source.replaceAll('if (', '/* Condition */\nif (')), + original, + ); + const changed = await findings( + source.replace('first ${value.name}', 'changed ${value.name}'), + ); + assert.ok( + changed.every(({ key }) => !original.some((entry) => entry.key === key)), + ); + const jsxChanged = await findings(source.replace('>second<', '>different<')); + assert.ok( + jsxChanged.every(({ key }) => !original.some((entry) => entry.key === key)), + ); +}); + +/** Equal bodies under the same lexical owner have no trustworthy correspondence, and duplicate reports cannot invent an ordinal fallback. */ +test('indistinguishable callbacks and duplicate diagnostics fail closed', async () => { + await assert.rejects( + () => findings(`run(${callback(2)}); run(${callback(2)});`), + /Ambiguous lexical scope/, + ); + const reports = await analyze(`const handler = wrap(${callback(2)});`); + reports[0].messages.push(reports[0].messages[0]); + assert.throws( + () => eslintRecords(reports), + /Duplicate ESLint diagnostic identity/, + ); +}); + +/** Separate deep branches have independent syntax owners, so a function cannot lend its deepest block's allowance to another block. */ +test('nested depth reports are keyed by syntax instead of diagnostic ordinal', async () => { + const source = + 'function check(value) { if (value.a) { if (value.b) { if (value.c) return 1; } } if (value.d) { if (value.e) { if (value.f) { if (value.g) return 2; } } } }'; + const records = await findings(source, 'max-depth', 2); + assert.ok(records.length > 1); + assert.equal(new Set(records.map(({ key }) => key)).size, records.length); +}); + +/** Recovered ASTs and missing function locations cannot silently attach findings to file-level debt. */ +test('invalid or missing scope evidence fails closed', () => { + const message = { + ruleId: 'complexity', + message: 'Arrow function has a complexity of 3. Maximum allowed is 2.', + line: 1, + column: 1, + }; + assert.throws( + () => + eslintRecords([ + { filePath: 'missing.js', source: '', messages: [message] }, + ]), + /No lexical scope/, + ); + assert.throws( + () => + eslintRecords([ + { + filePath: 'invalid.js', + source: 'function broken( {', + messages: [message], + }, + ]), + /Invalid syntax/, + ); +}); From f08faee18b7dcd9efddc21e89031b4171c64e8e6 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 01:14:57 +0500 Subject: [PATCH 05/11] ci(quality): replay sealed lexical scope analysis --- tools/quality/README.md | 12 ++++- tools/quality/lib/reports.mjs | 26 +++++++++- tools/quality/package-lock.json | 4 +- tools/quality/package.json | 2 +- tools/quality/template-manifest.json | 14 +++--- tools/quality/test/cli.test.mjs | 74 ++++++++++++++++++++++++++++ tools/quality/test/reports.test.mjs | 46 +++++++++++++++++ 7 files changed, 165 insertions(+), 13 deletions(-) diff --git a/tools/quality/README.md b/tools/quality/README.md index 4b600bd..aa67e5b 100644 --- a/tools/quality/README.md +++ b/tools/quality/README.md @@ -1,4 +1,4 @@ -# 1MSG quality template 1.0.8 +# 1MSG quality template 1.0.9 Vendored, private template for 1MSG-156. Install its exact tools with `npm --prefix tools/quality ci` on Node 24.19.0. The lockfile and @@ -108,3 +108,13 @@ or indistinguishable groups fail closed. The real CLI fixtures verify both legitimate debt reduction and rejection of transferred/increased allowances. Existing consumers must replay historical source with this version when adopting the new key schema; old diagnostic ordinals are never silently migrated. + +## 1.0.9 migration + +Complexity identities now retain lexical `for`, `for-in` and `for-of` headers +(binding, source/condition/increment and `await` mode). Identical statements in +different loops no longer collide. Loop bodies, source lines and diagnostic +ordinals do not identify allowances; indistinguishable sibling loops still fail +closed. Consumers must replay their original trusted adoption base with the +updated analyzer, preserving debt owners, deadlines and reasons. Do not translate +baseline keys manually or renew historical debt as part of the version update. diff --git a/tools/quality/lib/reports.mjs b/tools/quality/lib/reports.mjs index b04abef..b70336f 100644 --- a/tools/quality/lib/reports.mjs +++ b/tools/quality/lib/reports.mjs @@ -244,7 +244,7 @@ function ownerName(node, source) { return ['computed', syntaxFingerprint(node, source)]; } -/** Include every named lexical owner and each enclosing call's argument position, without using source lines or diagnostic order. */ +/** Include named owners, lexical loop bindings and call argument positions without source lines or diagnostic order. */ function lexicalOwners(node, source) { const owners = []; let child; @@ -253,7 +253,29 @@ function lexicalOwners(node, source) { current && !ts.isSourceFile(current); current = current.parent ) { - if (ts.isCallExpression(current) || ts.isNewExpression(current)) { + if (ts.isForOfStatement(current) || ts.isForInStatement(current)) { + owners.push([ + ts.SyntaxKind[current.kind], + syntaxFingerprint(current.initializer, source), + syntaxFingerprint(current.expression, source), + ts.isForOfStatement(current) && current.awaitModifier + ? 'await' + : 'sync', + child === current.statement ? 'body' : 'header', + ]); + } else if (ts.isForStatement(current)) { + owners.push([ + ts.SyntaxKind[current.kind], + current.initializer + ? syntaxFingerprint(current.initializer, source) + : null, + current.condition ? syntaxFingerprint(current.condition, source) : null, + current.incrementor + ? syntaxFingerprint(current.incrementor, source) + : null, + child === current.statement ? 'body' : 'header', + ]); + } else if (ts.isCallExpression(current) || ts.isNewExpression(current)) { const argument = current.arguments?.indexOf(child) ?? -1; owners.push([ ts.SyntaxKind[current.kind], diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json index 8ad5519..0c6a588 100644 --- a/tools/quality/package-lock.json +++ b/tools/quality/package-lock.json @@ -1,12 +1,12 @@ { "name": "@1msg/quality-template", - "version": "1.0.8", + "version": "1.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@1msg/quality-template", - "version": "1.0.8", + "version": "1.0.9", "dependencies": { "@eslint/js": "9.39.5", "dependency-cruiser": "18.6.0", diff --git a/tools/quality/package.json b/tools/quality/package.json index b71d5b0..0620fb9 100644 --- a/tools/quality/package.json +++ b/tools/quality/package.json @@ -1,6 +1,6 @@ { "name": "@1msg/quality-template", - "version": "1.0.8", + "version": "1.0.9", "private": true, "type": "module", "engines": { diff --git a/tools/quality/template-manifest.json b/tools/quality/template-manifest.json index da93308..883a990 100644 --- a/tools/quality/template-manifest.json +++ b/tools/quality/template-manifest.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "version": "1.0.8", + "version": "1.0.9", "files": { - "README.md": "e74940b5acec1bdbff75eb751432967630ddb4215af24d89d31dd3694916c12e", + "README.md": "91dd028828d08424d26dd4f5fc1e91da5c2258a3604a2fab57658c0f1c59b745", "canonical-duplicates.mjs": "a3583fdc3e344af448ea8d2543c2398daaa03fa10a0739da486afb04e35425ba", "complexity.mjs": "4ed5553c392621e0615a48c12e12300c8602ed2a28cfbaff67002acf82e49cac", "eslint.mjs": "607ddfaac3d365fedd7dbc5e21c781d448347b1c36662f3492e4364ff9f22ded", @@ -11,17 +11,17 @@ "lib/guards.mjs": "e94dc6b58703b44420f8b815ee069b9e0a7fa9aefd5faba63d55f1c3da7c4286", "lib/historical.mjs": "43b46395828daa3f19d8692e9bd22ea6cfc1d0dbac83814d727894f672a0ddd2", "lib/knip-reporter.mjs": "175d7d96e2913e8f6de04b6c706172258d9d7654a26ba7f9ed7edff79d3fc8ca", - "lib/reports.mjs": "60e78d58256d1060067e939397c8760384f202625c394f73d18e3311ccabb44c", + "lib/reports.mjs": "e41c4994e444128ae0418eff7240f49de678a97112fa7d5171fbe680d26b6dff", "lib/template.mjs": "c2a14b715f0c9dbe8b8243b142f4c6628967400fb8f92767537169e1b7bfd8f4", - "package-lock.json": "fc4279a0f1393109958815b6b92fc95acd1c5a98387ea6d3f13a61a9aeba8a60", - "package.json": "3e9ef4c922c64deddd6c2c50350def2bf79cd0354af891e5d7051c082c4d4937", + "package-lock.json": "314de2cb801a00bb42240da3567274bac9725359c0429665c505653a51bf5f91", + "package.json": "e1228b0144b1d8bd79283bfacec0be9a54f636e973ba42de4b1cc3694972f57e", "prettier.config.mjs": "7576563a9a30aff8eb3177f9cc2e4bc7a0f6b825042eadb9a619b86f519c5e5e", "run.mjs": "7cf26128cdf63f3a7ccab7341a0a274a352fd9bda0521ba41e37216bb9cc249f", "seal.mjs": "fb38c8e08ea3cf9f4dcd95e45ffc931aea3b7a54181fed88d7379f8aad4c050d", - "test/cli.test.mjs": "64dda5d60ef9fd2d444f96b2623b97f6197917f1e121aaf73cbe1dfeff90a8ef", + "test/cli.test.mjs": "63350dcf9c58c0e41d0dcc49bb8b9eef8dd0c01e781f25a58490f1476f58e94a", "test/complexity.test.mjs": "ba1f702ba94de3fcad93cb5e43a49bc2456be1426c81754149420d210f2b3952", "test/ratchet.test.mjs": "324a932c5a52d1b037e111042e1b41e5686f8d4dbfd0307faf86cee649ebc27a", - "test/reports.test.mjs": "d75a93ee4dc606d9930c3a1e19912e093e8635910a7c21f81c735e209c03835f", + "test/reports.test.mjs": "3d5be0312d4870dd00e4cdafaa422d1e0d4e0d3ecaf4dc65a73a4dac577bfbfa", "tsconfig.next.json": "d900a35fcb0852376b06854df5be2f8f178e26cb5383b93b07ebeb6220e30faf", "tsconfig.node.json": "627661abcb7e992c4bfaf98d6e68a12678573b5d4f6e821ea0fa43fa6e233f28", "tsconfig.svelte.json": "5777247ea98aaf2d886b87cd12206a641377edcdd086ce5fd5b58adc6f0bc771" diff --git a/tools/quality/test/cli.test.mjs b/tools/quality/test/cli.test.mjs index 624b8ec..33425a7 100644 --- a/tools/quality/test/cli.test.mjs +++ b/tools/quality/test/cli.test.mjs @@ -749,3 +749,77 @@ test('actual complexity CLI resolves class field and static block scopes', () => f.close(); } }); + +/** A Make-shaped source has equal if statements in separate loops; trusted replay retains only the precise old depth allowance. */ +test('actual raw complexity CLI distinguishes lexical loops and rejects depth transfer', () => { + const f = fixture(); + try { + f.write( + 'eslint.config.mjs', + "export default [{rules:{'max-depth':['error',3]}}];", + ); + const shallow = + 'for (const ack of root.ack) { const item = record(ack); if (item) found.push(statusEvent(item)); }'; + const deep = + 'for (const entry of root.entry) { for (const change of entry.changes) { for (const status of change.statuses) { const item = record(status); if (item) found.push(statusEvent(item)); } } }'; + f.write('src/index.js', `function collect(root) { ${shallow} ${deep} }`); + const base = f.commit(); + const args = [ + 'report', + '--kind', + 'eslint', + '--base', + base, + '--bootstrap', + '--owner', + 'quality-fixture', + '--expires', + '2099-12-31', + '--reason', + 'Historical lexical loop depth', + '--issue', + 'TEST-1', + '--', + process.execPath, + fileURLToPath( + new URL('../node_modules/eslint/bin/eslint.js', import.meta.url), + ), + '--config', + 'eslint.config.mjs', + '--no-inline-config', + '--format', + 'json', + 'src/index.js', + ]; + const original = f.run(...args); + assert.equal(original.status, 0, original.stderr); + const old = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.equal(old.entries.length, 1); + assert.equal(old.entries[0].value, 4); + f.write('src/index.js', `function collect(root) { ${deep} }`); + const reduced = f.run(...args); + assert.equal(reduced.status, 0, reduced.stderr); + const retained = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.equal(retained.entries[0].key, old.entries[0].key); + f.write( + 'src/index.js', + `function collect(root) { if (root.ready) { if (root.active) { ${shallow} } } }`, + ); + const transfer = f.run(...args); + assert.equal(transfer.status, 1, transfer.stderr); + assert.match(transfer.stderr, /Cannot baseline a new\/increased finding/); + f.write( + 'src/index.js', + `function collect(root) { if (root.ready) { ${deep} } }`, + ); + const growth = f.run(...args); + assert.equal(growth.status, 1, growth.stderr); + assert.match(growth.stderr, /Cannot baseline a new\/increased finding/); + } finally { + f.close(); + } +}); diff --git a/tools/quality/test/reports.test.mjs b/tools/quality/test/reports.test.mjs index 68c39e2..053a7df 100644 --- a/tools/quality/test/reports.test.mjs +++ b/tools/quality/test/reports.test.mjs @@ -190,3 +190,49 @@ test('invalid or missing scope evidence fails closed', () => { /Invalid syntax/, ); }); + +/** Equal statements under distinct lexical loop bindings are independently addressable even when only one is over threshold. */ +test('loop headers distinguish nesting and callback owners without body ordinals', async () => { + const source = `function collect(root) { + for (const ack of root.ack) { const item = record(ack); if (item) found.push(statusEvent(item)); } + for (const entry of root.entry) { for (const change of entry.changes) { + for (const status of change.statuses) { const item = record(status); if (item) found.push(statusEvent(item)); } + } } + }`; + const original = await findings(source, 'max-depth', 3); + assert.equal(original.length, 1); + assert.equal(original[0].value, 4); + assert.deepEqual( + await findings( + source.replace('const ack', '/* Header documentation */ const ack'), + 'max-depth', + 3, + ), + original, + ); + const callbacks = `for (const ack of root.ack) { run(${callback(2)}); } + for (const status of root.statuses) { run(${callback(2)}); } + for (const key in root.properties) { run(${callback(2)}); } + for (let index = 0; index < root.length; index++) { run(${callback(2)}); }`; + const owned = await findings(callbacks); + assert.equal(owned.length, 4); + assert.equal(new Set(owned.map(({ key }) => key)).size, 4); + const asynchronous = await findings( + `for await (const ack of root.ack) { run(${callback(2)}); }`, + ); + assert.equal(asynchronous.length, 1); + assert.ok(!owned.some(({ key }) => key === asynchronous[0].key)); + const shifted = await findings( + callbacks.replace('const ack of root.ack', 'const other of root.ack'), + ); + assert.equal( + shifted.filter(({ key }) => !owned.some((old) => old.key === key)).length, + 1, + ); +}); + +/** Repeated loops with identical headers remain ambiguous; distinct bindings must never enable ordinal-based allowance borrowing. */ +test('identical loop bindings do not disambiguate indistinguishable callbacks', async () => { + const loop = `for (const item of root.items) { run(${callback(2)}); }`; + await assert.rejects(() => findings(loop + loop), /Ambiguous lexical scope/); +}); From bdaee29829d1ef93b3b489060248260a24d292c8 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 02:08:49 +0500 Subject: [PATCH 06/11] chore(quality): adopt sealed analyzer 1.0.10 --- tools/quality/README.md | 21 +++- tools/quality/lib/guards.mjs | 18 +++- tools/quality/lib/reports.mjs | 110 ++++++++++++++++++++- tools/quality/package-lock.json | 4 +- tools/quality/package.json | 2 +- tools/quality/template-manifest.json | 17 ++-- tools/quality/test/cli.test.mjs | 80 +++++++++++++++ tools/quality/test/guard-history.test.mjs | 113 ++++++++++++++++++++++ tools/quality/test/reports.test.mjs | 106 ++++++++++++++++++++ 9 files changed, 456 insertions(+), 15 deletions(-) create mode 100644 tools/quality/test/guard-history.test.mjs diff --git a/tools/quality/README.md b/tools/quality/README.md index aa67e5b..2e4def1 100644 --- a/tools/quality/README.md +++ b/tools/quality/README.md @@ -1,4 +1,4 @@ -# 1MSG quality template 1.0.9 +# 1MSG quality template 1.0.10 Vendored, private template for 1MSG-156. Install its exact tools with `npm --prefix tools/quality ci` on Node 24.19.0. The lockfile and @@ -118,3 +118,22 @@ ordinals do not identify allowances; indistinguishable sibling loops still fail closed. Consumers must replay their original trusted adoption base with the updated analyzer, preserving debt owners, deadlines and reasons. Do not translate baseline keys manually or renew historical debt as part of the version update. + +## 1.0.10 migration + +Otherwise identical sibling scopes use the nearest distinguishing parsed branch or +block context, including then/else, loop, switch and try/catch/finally roles. This +fallback leaves already unambiguous identities unchanged. The complete ordered +peer evidence remains part of an ambiguous group's identity; swaps, new peers, +growth and fully indistinguishable ancestry cannot borrow another allowance. +ESLint object-property diagnostics resolve the exact property name to its direct +callable initializer. Previously misattributed nested callback metrics now belong +to that callback, rather than its enclosing function. Consumers must replay their +original trusted adoption base; preserve owners, expiry and reasons and never +translate baseline keys manually. + +For function documentation only, a syntax error in trusted historical source +removes all inherited function signatures: every current function in that file +must have documentation. Current syntax errors and historical/current I/O, +configuration or Git failures remain fatal. This conservative documentation +fallback does not alter metric analysis, source coverage or debt baselines. diff --git a/tools/quality/lib/guards.mjs b/tools/quality/lib/guards.mjs index f27c583..69e3bde 100644 --- a/tools/quality/lib/guards.mjs +++ b/tools/quality/lib/guards.mjs @@ -211,12 +211,15 @@ function callbackComments(node, ast) { return comments; } +/** Identify source syntax failures without conflating analyzer, configuration or I/O errors. */ +class SourceParseError extends SyntaxError {} + /** Extract all implemented functions, including private methods, factories and substantive callbacks. */ export function functionsIn(path, text) { const functions = []; for (const { suffix, ast } of parseUnits(path, text)) { if (ast.parseDiagnostics.length) - throw new Error( + throw new SourceParseError( `Cannot parse ${path}: ${ast.parseDiagnostics[0].messageText}`, ); const visit = (node) => { @@ -266,6 +269,17 @@ export function functionsIn(path, text) { return functions; } +/** Invalid historical syntax earns no unchanged-function allowance; every current function must satisfy the documentation contract. */ +function historicalFunctions(path, text) { + if (text === null) return []; + try { + return functionsIn(path, text); + } catch (error) { + if (error instanceof SourceParseError) return []; + throw error; + } +} + /** Identify suppression directives in comments only, never in strings or generated documentation examples. */ export function suppressionErrors(path, text, config) { const errors = []; @@ -358,7 +372,7 @@ export function runGuards(config, base) { continue; const before = baseFile(base, path); const previous = new Map(); - for (const fn of before === null ? [] : functionsIn(path, before)) { + for (const fn of historicalFunctions(path, before)) { const identity = `${fn.key}:${fn.signature}`; previous.set(identity, (previous.get(identity) ?? 0) + 1); } diff --git a/tools/quality/lib/reports.mjs b/tools/quality/lib/reports.mjs index b70336f..4cec53b 100644 --- a/tools/quality/lib/reports.mjs +++ b/tools/quality/lib/reports.mjs @@ -329,6 +329,92 @@ function isDepthScope(node) { ].includes(node.kind); } +/** Identify a control-flow child by its semantic role, never its position among statements. */ +function branchRole(parent, child) { + if (ts.isIfStatement(parent) || ts.isConditionalExpression(parent)) { + if (child === (parent.thenStatement ?? parent.whenTrue)) return 'then'; + if (child === (parent.elseStatement ?? parent.whenFalse)) return 'else'; + return 'condition'; + } + if (ts.isTryStatement(parent)) { + if (child === parent.tryBlock) return 'try'; + if (child === parent.catchClause) return 'catch'; + return 'finally'; + } + if (ts.isCatchClause(parent)) + return child === parent.block ? 'body' : 'binding'; + if (ts.isCaseClause(parent)) + return child === parent.expression ? 'case-value' : 'case-body'; + if (ts.isDefaultClause(parent)) return 'default-body'; + if (ts.isSwitchStatement(parent)) + return child === parent.caseBlock ? 'cases' : 'expression'; + if (isDepthScope(parent)) + return child === parent.statement ? 'body' : 'header'; + return 'block'; +} + +/** Collect parsed enclosing branches only for otherwise indistinguishable peers; literal values and then/else roles remain evidence. */ +function structuralContext(node, source) { + const contexts = []; + let child = node; + for ( + let parent = node.parent; + parent && !ts.isSourceFile(parent); + parent = parent.parent + ) { + if ( + ts.isBlock(parent) || + isDepthScope(parent) || + ts.isConditionalExpression(parent) || + ts.isCatchClause(parent) || + ts.isCaseClause(parent) || + ts.isDefaultClause(parent) + ) { + contexts.push([ + ts.SyntaxKind[parent.kind], + branchRole(parent, child), + syntaxFingerprint(parent, source), + ]); + } + child = parent; + } + return contexts; +} + +/** Find the shortest unique ancestor prefix among equal bodies; completely equal ancestry still has no safe correspondence. */ +function distinctContexts(nodes, fingerprints, source) { + const contexts = nodes.map( + /** Avoid introducing contextual identities for a body already distinguished by its own syntax. */ + (node, index) => + fingerprints.indexOf(fingerprints[index]) === + fingerprints.lastIndexOf(fingerprints[index]) + ? [] + : structuralContext(node, source), + ); + return contexts.map( + /** Select the shortest semantic ancestor evidence that separates this body from its equal peers. */ ( + context, + index, + ) => { + const peers = fingerprints.flatMap((fingerprint, peer) => + fingerprint === fingerprints[index] && peer !== index ? [peer] : [], + ); + if (!peers.length) return null; + for (let length = 1; length <= context.length; length++) { + const candidate = JSON.stringify(context.slice(0, length)); + if ( + peers.every( + (peer) => + JSON.stringify(contexts[peer].slice(0, length)) !== candidate, + ) + ) + return candidate; + } + return null; + }, + ); +} + /** Index all lexical scopes, including functions below lint thresholds, so disappearing diagnostics cannot renumber another allowance. */ function scopeIdentities(source, depth = false) { const groups = new Map(); @@ -360,6 +446,15 @@ function scopeIdentities(source, depth = false) { const fingerprints = nodes.map((node) => syntaxFingerprint(node, source)); // Preserve source order in ambiguous groups: swapping a large and small body must not transfer the large allowance. const group = digest(JSON.stringify(fingerprints)); + const contexts = distinctContexts(nodes, fingerprints, source); + const contextualGroup = digest( + JSON.stringify( + fingerprints.map((fingerprint, index) => [ + fingerprint, + contexts[index], + ]), + ), + ); for (const [index, node] of nodes.entries()) { const fingerprint = fingerprints[index]; // A changed ambiguous group loses every old allowance; even matching another old body cannot borrow its budget. @@ -368,7 +463,9 @@ function scopeIdentities(source, depth = false) { fingerprints.indexOf(fingerprint) === fingerprints.lastIndexOf(fingerprint) ? `${owner}|group:${group}|member:${fingerprint}` - : null, + : contexts[index] === null + ? null + : `${owner}|context-group:${contextualGroup}|member:${fingerprint}|context:${digest(contexts[index])}`, ); } } @@ -386,6 +483,17 @@ function scopeAt(source, identities, line, column) { function visit(node) { if (node.getStart(source) <= offset && node.end > offset) { if (identities.has(node)) matched = node; + // ESLint complexity locates a named object property before its arrow/function initializer. + // Resolve only that direct callable initializer, never an unrelated neighboring function. + if ( + ts.isPropertyAssignment(node) && + ts.isFunctionLike(node.initializer) && + identities.has(node.initializer) && + offset >= node.name.getStart(source) && + offset < node.name.end + ) { + matched = node.initializer; + } ts.forEachChild(node, visit); } } diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json index 0c6a588..5e6bab1 100644 --- a/tools/quality/package-lock.json +++ b/tools/quality/package-lock.json @@ -1,12 +1,12 @@ { "name": "@1msg/quality-template", - "version": "1.0.9", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@1msg/quality-template", - "version": "1.0.9", + "version": "1.0.10", "dependencies": { "@eslint/js": "9.39.5", "dependency-cruiser": "18.6.0", diff --git a/tools/quality/package.json b/tools/quality/package.json index 0620fb9..07e7cfe 100644 --- a/tools/quality/package.json +++ b/tools/quality/package.json @@ -1,6 +1,6 @@ { "name": "@1msg/quality-template", - "version": "1.0.9", + "version": "1.0.10", "private": true, "type": "module", "engines": { diff --git a/tools/quality/template-manifest.json b/tools/quality/template-manifest.json index 883a990..f2cf559 100644 --- a/tools/quality/template-manifest.json +++ b/tools/quality/template-manifest.json @@ -1,27 +1,28 @@ { "schemaVersion": 1, - "version": "1.0.9", + "version": "1.0.10", "files": { - "README.md": "91dd028828d08424d26dd4f5fc1e91da5c2258a3604a2fab57658c0f1c59b745", + "README.md": "50df5c60db609380718c9228de8f8f53cf1949673bd66f71a9f0192fae1d6dbd", "canonical-duplicates.mjs": "a3583fdc3e344af448ea8d2543c2398daaa03fa10a0739da486afb04e35425ba", "complexity.mjs": "4ed5553c392621e0615a48c12e12300c8602ed2a28cfbaff67002acf82e49cac", "eslint.mjs": "607ddfaac3d365fedd7dbc5e21c781d448347b1c36662f3492e4364ff9f22ded", "lib/baseline.mjs": "7876ba90b80448d9c9003c21e348095ed22630c41fc26ed944984742020549dd", "lib/git.mjs": "b7c26871526d864bf8d12f01c613d3bfa0adb2da9d072544d8f09407ef598758", - "lib/guards.mjs": "e94dc6b58703b44420f8b815ee069b9e0a7fa9aefd5faba63d55f1c3da7c4286", + "lib/guards.mjs": "b1b025a1fdcb63434733216a4faaa45112aa404f26f9d31df28921243d6a22f8", "lib/historical.mjs": "43b46395828daa3f19d8692e9bd22ea6cfc1d0dbac83814d727894f672a0ddd2", "lib/knip-reporter.mjs": "175d7d96e2913e8f6de04b6c706172258d9d7654a26ba7f9ed7edff79d3fc8ca", - "lib/reports.mjs": "e41c4994e444128ae0418eff7240f49de678a97112fa7d5171fbe680d26b6dff", + "lib/reports.mjs": "361e38ae981238db91f7b9a31af963ccbb89e3ae6745d1582b2086e6c64f0c9e", "lib/template.mjs": "c2a14b715f0c9dbe8b8243b142f4c6628967400fb8f92767537169e1b7bfd8f4", - "package-lock.json": "314de2cb801a00bb42240da3567274bac9725359c0429665c505653a51bf5f91", - "package.json": "e1228b0144b1d8bd79283bfacec0be9a54f636e973ba42de4b1cc3694972f57e", + "package-lock.json": "85c154a1d50a55d975a56f173a25eb5be3ef6ae7ffa111726531e31246621907", + "package.json": "188e7badefdaa40018946b7d320f8c87385ba146ead7e404b3abc5ec371eb249", "prettier.config.mjs": "7576563a9a30aff8eb3177f9cc2e4bc7a0f6b825042eadb9a619b86f519c5e5e", "run.mjs": "7cf26128cdf63f3a7ccab7341a0a274a352fd9bda0521ba41e37216bb9cc249f", "seal.mjs": "fb38c8e08ea3cf9f4dcd95e45ffc931aea3b7a54181fed88d7379f8aad4c050d", - "test/cli.test.mjs": "63350dcf9c58c0e41d0dcc49bb8b9eef8dd0c01e781f25a58490f1476f58e94a", + "test/cli.test.mjs": "bd3445143a45aabba2a95593aace73f96223389f7a440c2ef41bf639bb4ec12a", "test/complexity.test.mjs": "ba1f702ba94de3fcad93cb5e43a49bc2456be1426c81754149420d210f2b3952", + "test/guard-history.test.mjs": "b699cc394b9c843a59798f28fe2b5dd6c199c3f3e16dc1c61c35724a935fc23e", "test/ratchet.test.mjs": "324a932c5a52d1b037e111042e1b41e5686f8d4dbfd0307faf86cee649ebc27a", - "test/reports.test.mjs": "3d5be0312d4870dd00e4cdafaa422d1e0d4e0d3ecaf4dc65a73a4dac577bfbfa", + "test/reports.test.mjs": "521a13da2f16628b4a30de7040529e0b7b7e25e7db79d19de8004945d49c5e0c", "tsconfig.next.json": "d900a35fcb0852376b06854df5be2f8f178e26cb5383b93b07ebeb6220e30faf", "tsconfig.node.json": "627661abcb7e992c4bfaf98d6e68a12678573b5d4f6e821ea0fa43fa6e233f28", "tsconfig.svelte.json": "5777247ea98aaf2d886b87cd12206a641377edcdd086ce5fd5b58adc6f0bc771" diff --git a/tools/quality/test/cli.test.mjs b/tools/quality/test/cli.test.mjs index 33425a7..92c7b32 100644 --- a/tools/quality/test/cli.test.mjs +++ b/tools/quality/test/cli.test.mjs @@ -823,3 +823,83 @@ test('actual raw complexity CLI distinguishes lexical loops and rejects depth tr f.close(); } }); + +/** Raw historical replay resolves Core-shaped duplicate try blocks while rejecting swaps, added scopes and increased nesting. */ +test('actual complexity CLI validates structural context without transferring debt', () => { + const f = fixture(); + try { + f.write( + 'eslint.config.mjs', + "export default [{rules:{'max-depth':['error',3]}}];", + ); + const text = + 'if (match) { if (template.rejected) { try { send(template); } catch (error) { record(error); } } else { saveText(template); } }'; + const document = text.replace('saveText', 'saveDocument'); + /** Retain branch code while varying only a surrounding while depth or branch sequence. */ + const source = (loops, branches) => + `function collect(root) { ${'while (root.ready) {'.repeat(loops)} if (root.templates) { for (const template of root.templates) { ${branches.join('\n')} } } ${'}'.repeat(loops)} }`; + f.write('src/index.js', source(2, [text, document])); + const base = f.commit(); + const args = [ + 'report', + '--kind', + 'eslint', + '--base', + base, + '--bootstrap', + '--owner', + 'quality-fixture', + '--expires', + '2099-12-31', + '--reason', + 'Historical structural branch depth', + '--issue', + 'TEST-1', + '--', + process.execPath, + fileURLToPath( + new URL('../node_modules/eslint/bin/eslint.js', import.meta.url), + ), + '--config', + 'eslint.config.mjs', + '--no-inline-config', + '--format', + 'json', + 'src/index.js', + ]; + const original = f.run(...args); + assert.equal(original.status, 0, original.stderr); + const old = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.ok(old.entries.some(({ key }) => key.includes('context-group:'))); + f.write('src/index.js', source(1, [text, document])); + const reduced = f.run(...args); + assert.equal(reduced.status, 0, reduced.stderr); + const retained = JSON.parse( + readFileSync(join(f.cwd, 'quality/baselines/complexity.json'), 'utf8'), + ); + assert.ok( + retained.entries.every((entry) => + old.entries.some( + (prior) => prior.key === entry.key && prior.value >= entry.value, + ), + ), + ); + for (const current of [ + source(2, [document, text]), + source(2, [text, document, text.replace('saveText', 'saveImage')]), + source(3, [text, document]), + ]) { + f.write('src/index.js', current); + const violation = f.run(...args); + assert.equal(violation.status, 1, violation.stderr); + assert.match( + violation.stderr, + /Cannot baseline a new\/increased finding/, + ); + } + } finally { + f.close(); + } +}); diff --git a/tools/quality/test/guard-history.test.mjs b/tools/quality/test/guard-history.test.mjs new file mode 100644 index 0000000..b49ffa7 --- /dev/null +++ b/tools/quality/test/guard-history.test.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const runner = fileURLToPath(new URL('../run.mjs', import.meta.url)); + +/** Build independent Git history so the actual CLI must distinguish trusted historical syntax from current source failures. */ +function fixture() { + const cwd = mkdtempSync(join(tmpdir(), 'quality-guard-history-')); + const env = { ...process.env, CI: '', QUALITY_BASE: undefined }; + for (const name of Object.keys(env)) { + if (name.startsWith('GIT_')) delete env[name]; + } + env.GIT_CONFIG_GLOBAL = process.platform === 'win32' ? 'NUL' : '/dev/null'; + env.GIT_CONFIG_NOSYSTEM = '1'; + mkdirSync(join(cwd, 'src')); + writeFileSync( + join(cwd, 'quality.config.mjs'), + "export default {schemaVersion:1,profile:'B',defaultBranch:'main',sourceRoots:['src','quality.config.mjs']};\n", + ); + const git = (...args) => { + const result = spawnSync('git', args, { cwd, env, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + git('init', '-b', 'main'); + git('config', 'user.name', 'Quality fixture'); + git('config', 'user.email', 'quality@example.invalid'); + return { + cwd, + write: (text) => writeFileSync(join(cwd, 'src/Widget.svelte'), text), + commit: () => { + git('add', '.'); + git('commit', '-m', 'test: historical syntax'); + return git('rev-parse', 'HEAD'); + }, + run: (base) => + spawnSync(process.execPath, [runner, 'guards', '--base', base], { + cwd, + env, + encoding: 'utf8', + timeout: 30_000, + }), + close: () => rmSync(cwd, { recursive: true, force: true }), + }; +} + +test('actual guards require all current docs after unparseable trusted Svelte history', () => { + const f = fixture(); + try { + // Historical markup embeds script text in a JavaScript template; the old script extractor cannot parse that extra unit. + f.write( + '\n', + ); + const base = f.commit(); + f.write( + '\n

Readable widget

', + ); + const undocumented = f.run(base); + assert.notEqual(undocumented.status, 0); + assert.match(undocumented.stderr, /document stable/); + assert.match(undocumented.stderr, /document changed/); + assert.doesNotMatch(undocumented.stderr, /Cannot parse/); + + f.write( + '\n

Readable widget

', + ); + const documented = f.run(base); + assert.equal(documented.status, 0, documented.stderr); + assert.match(documented.stdout, /guards: PASS/); + + f.write(''); + const invalidCurrent = f.run(base); + assert.notEqual(invalidCurrent.status, 0); + assert.match(invalidCurrent.stderr, /Cannot parse src\/Widget\.svelte/); + + const invalidBase = f.run('nonexistent-trusted-reference'); + assert.notEqual(invalidBase.status, 0); + assert.match(invalidBase.stderr, /git rev-parse failed/); + } finally { + f.close(); + } +}); + +test('historical fallback does not consume source I/O or configuration failures', () => { + const f = fixture(); + try { + f.write(''); + const base = f.commit(); + rmSync(join(f.cwd, 'src/Widget.svelte')); + mkdirSync(join(f.cwd, 'src/Widget.svelte')); + const invalidInput = f.run(base); + assert.notEqual(invalidInput.status, 0); + assert.match( + invalidInput.stderr, + /EISDIR|illegal operation on a directory/, + ); + + writeFileSync( + join(f.cwd, 'quality.config.mjs'), + 'export default {schemaVersion:1,profile:"unknown",sourceRoots:["src"]};', + ); + const invalidConfig = f.run(base); + assert.notEqual(invalidConfig.status, 0); + assert.match(invalidConfig.stderr, /concrete profile/); + } finally { + f.close(); + } +}); diff --git a/tools/quality/test/reports.test.mjs b/tools/quality/test/reports.test.mjs index 053a7df..339ee55 100644 --- a/tools/quality/test/reports.test.mjs +++ b/tools/quality/test/reports.test.mjs @@ -236,3 +236,109 @@ test('identical loop bindings do not disambiguate indistinguishable callbacks', const loop = `for (const item of root.items) { run(${callback(2)}); }`; await assert.rejects(() => findings(loop + loop), /Ambiguous lexical scope/); }); + +/** Equal try blocks in text/document template branches need their differing surrounding branch bodies as evidence. */ +test('identical scopes use the nearest distinguishing structural branch context', async () => { + const source = `function templates(root) { + while (root.ready) { if (root.templates) { for (const template of root.templates) { + if (match) { if (template.rejected) { try { send(template); } catch (error) { record(error); } } else { saveText(template); } } + if (match) { if (template.rejected) { try { send(template); } catch (error) { record(error); } } else { saveDocument(template); } } + } } } + }`; + const records = await findings(source, 'max-depth', 3); + assert.ok(records.some(({ key }) => key.includes('context-group:'))); + assert.equal(new Set(records.map(({ key }) => key)).size, records.length); + const callbacks = `if (ready) { run(${callback(2)}); } else { run(${callback(2)}); } + try { run(${callback(2)}); } catch (error) { run(${callback(2)}); } finally { run(${callback(2)}); } + switch (kind) { case 'first': run(${callback(2)}); break; case 'second': run(${callback(2)}); break; default: run(${callback(2)}); } + while (active) { run(${callback(2)}); } + do { run(${callback(2)}); } while (active);`; + const owned = await findings(callbacks); + assert.equal(owned.length, 10); + assert.equal(new Set(owned.map(({ key }) => key)).size, 10); + assert.deepEqual( + await findings( + callbacks.replaceAll('run(', '/* Branch documentation */ run('), + ), + owned, + ); +}); + +/** Identical branches and predicates provide no trustworthy address merely because they appear at different source offsets. */ +test('identical structural ancestry still fails closed', async () => { + const branch = `if (ready) { run(${callback(2)}); }`; + await assert.rejects( + () => findings(branch + branch), + /Ambiguous lexical scope/, + ); +}); + +/** ESLint names object-property callbacks at the property token, which precedes the actual function start. */ +test('object property diagnostics resolve only their direct callable initializer', async () => { + for (const fn of [ + `async ${callback(2)}`, + 'async function(value) { if (value.p0) return 1; if (value.p1) return 2; return 0; }', + ]) { + const source = `const endpoints = { handler: ${fn}, unrelated: 1 };`; + const report = await analyze(source); + assert.equal(report[0].messages[0].column, source.indexOf('handler') + 1); + const records = eslintRecords(report); + assert.equal(records.length, 1); + assert.match(records[0].key, /handler/); + assert.deepEqual( + await findings(source.replace('handler:', 'handler: /* Contract */')), + records, + ); + for (const column of [ + source.indexOf('handler:') + 'handler'.length + 1, + source.indexOf('unrelated') + 1, + ]) { + assert.throws( + () => + eslintRecords([ + { + filePath: 'invalid-location.js', + source, + messages: [ + { + ruleId: 'complexity', + line: 1, + column, + message: 'Function has a complexity of 4.', + }, + ], + }, + ]), + /No lexical scope/, + ); + } + } + assert.throws( + () => + eslintRecords([ + { + filePath: 'invalid-location.js', + source: 'const settings = { threshold: 5 };', + messages: [ + { + ruleId: 'complexity', + line: 1, + column: 20, + message: 'Function has a complexity of 4.', + }, + ], + }, + ]), + /No lexical scope/, + ); +}); + +/** An object callback diagnostic inside another function must not consume that enclosing function's complexity allowance. */ +test('object callbacks retain their own budget inside enclosing functions', async () => { + const source = `function useCheckout() { return useMutation({ onError: ${callback(2)} }); }`; + const report = await analyze(source); + assert.equal(report[0].messages.length, 1); + const record = eslintRecords(report)[0]; + assert.match(record.scope, /PropertyAssignment.*onError.*ArrowFunction/); + assert.equal(record.value, 3); +}); From 2e2d07f5c74815af1aa085aadee884df18b786e3 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 02:57:03 +0500 Subject: [PATCH 07/11] fix(quality): support older Git in shared fixtures --- tools/quality/README.md | 15 ++- tools/quality/package-lock.json | 4 +- tools/quality/package.json | 2 +- tools/quality/template-manifest.json | 13 +-- tools/quality/test/cli.test.mjs | 3 +- tools/quality/test/git-portability.test.mjs | 100 ++++++++++++++++++++ tools/quality/test/guard-history.test.mjs | 3 +- 7 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 tools/quality/test/git-portability.test.mjs diff --git a/tools/quality/README.md b/tools/quality/README.md index 2e4def1..9c78337 100644 --- a/tools/quality/README.md +++ b/tools/quality/README.md @@ -1,4 +1,4 @@ -# 1MSG quality template 1.0.10 +# 1MSG quality template 1.0.11 Vendored, private template for 1MSG-156. Install its exact tools with `npm --prefix tools/quality ci` on Node 24.19.0. The lockfile and @@ -137,3 +137,16 @@ removes all inherited function signatures: every current function in that file must have documentation. Current syntax errors and historical/current I/O, configuration or Git failures remain fatal. This conservative documentation fallback does not alter metric analysis, source coverage or debt baselines. + +## 1.0.11 migration + +Test repositories now initialize their default branch with `git init` followed by +`git symbolic-ref HEAD refs/heads/main`. This works on the older Git installation +used by the self-hosted runner, which does not support `git init -b`. A regression +fixture rejects that unsupported option and replays both actual Git-backed test +families through a subprocess-only Node preload shim; all other Git operations +remain real. The fixture uses no Unix executable or shebang assumptions and does +not modify the runtime Git driver or skip checks on Windows. +Only fixture setup changes: analysis rules, historical identities and baseline +records are unchanged. Copy the complete sealed version without changing debt +owners, reasons, issue links, expiry dates or baseline entries. diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json index 5e6bab1..527d33a 100644 --- a/tools/quality/package-lock.json +++ b/tools/quality/package-lock.json @@ -1,12 +1,12 @@ { "name": "@1msg/quality-template", - "version": "1.0.10", + "version": "1.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@1msg/quality-template", - "version": "1.0.10", + "version": "1.0.11", "dependencies": { "@eslint/js": "9.39.5", "dependency-cruiser": "18.6.0", diff --git a/tools/quality/package.json b/tools/quality/package.json index 07e7cfe..67942c0 100644 --- a/tools/quality/package.json +++ b/tools/quality/package.json @@ -1,6 +1,6 @@ { "name": "@1msg/quality-template", - "version": "1.0.10", + "version": "1.0.11", "private": true, "type": "module", "engines": { diff --git a/tools/quality/template-manifest.json b/tools/quality/template-manifest.json index f2cf559..210f229 100644 --- a/tools/quality/template-manifest.json +++ b/tools/quality/template-manifest.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "version": "1.0.10", + "version": "1.0.11", "files": { - "README.md": "50df5c60db609380718c9228de8f8f53cf1949673bd66f71a9f0192fae1d6dbd", + "README.md": "6760e252841ba82078ede1589a474dd19c8f77f828245e181f34e842465c3aa1", "canonical-duplicates.mjs": "a3583fdc3e344af448ea8d2543c2398daaa03fa10a0739da486afb04e35425ba", "complexity.mjs": "4ed5553c392621e0615a48c12e12300c8602ed2a28cfbaff67002acf82e49cac", "eslint.mjs": "607ddfaac3d365fedd7dbc5e21c781d448347b1c36662f3492e4364ff9f22ded", @@ -13,14 +13,15 @@ "lib/knip-reporter.mjs": "175d7d96e2913e8f6de04b6c706172258d9d7654a26ba7f9ed7edff79d3fc8ca", "lib/reports.mjs": "361e38ae981238db91f7b9a31af963ccbb89e3ae6745d1582b2086e6c64f0c9e", "lib/template.mjs": "c2a14b715f0c9dbe8b8243b142f4c6628967400fb8f92767537169e1b7bfd8f4", - "package-lock.json": "85c154a1d50a55d975a56f173a25eb5be3ef6ae7ffa111726531e31246621907", - "package.json": "188e7badefdaa40018946b7d320f8c87385ba146ead7e404b3abc5ec371eb249", + "package-lock.json": "9fc7d279d2bade1c42f807d99a107d77f15a8e42edd2a9b32b4b5ce1a4fda35f", + "package.json": "deb5fd5e972aec2926b21e31d02c47e9c35d507048e83ab0ed161425151546e2", "prettier.config.mjs": "7576563a9a30aff8eb3177f9cc2e4bc7a0f6b825042eadb9a619b86f519c5e5e", "run.mjs": "7cf26128cdf63f3a7ccab7341a0a274a352fd9bda0521ba41e37216bb9cc249f", "seal.mjs": "fb38c8e08ea3cf9f4dcd95e45ffc931aea3b7a54181fed88d7379f8aad4c050d", - "test/cli.test.mjs": "bd3445143a45aabba2a95593aace73f96223389f7a440c2ef41bf639bb4ec12a", + "test/cli.test.mjs": "49c55b1b4cf2ca2214b0f1b5d2bd973eac43005e14606f27a2a4aa682e0e691d", "test/complexity.test.mjs": "ba1f702ba94de3fcad93cb5e43a49bc2456be1426c81754149420d210f2b3952", - "test/guard-history.test.mjs": "b699cc394b9c843a59798f28fe2b5dd6c199c3f3e16dc1c61c35724a935fc23e", + "test/git-portability.test.mjs": "0e65759ff3a1aa87c20d20ae147154d980381c2e8075f57f341ef3c53c3b3f62", + "test/guard-history.test.mjs": "00b17403b3ef17cc5d83c681c990ca98b6a0ad4857a95d501d2cf8f390aa7b1e", "test/ratchet.test.mjs": "324a932c5a52d1b037e111042e1b41e5686f8d4dbfd0307faf86cee649ebc27a", "test/reports.test.mjs": "521a13da2f16628b4a30de7040529e0b7b7e25e7db79d19de8004945d49c5e0c", "tsconfig.next.json": "d900a35fcb0852376b06854df5be2f8f178e26cb5383b93b07ebeb6220e30faf", diff --git a/tools/quality/test/cli.test.mjs b/tools/quality/test/cli.test.mjs index 92c7b32..0c8d1cd 100644 --- a/tools/quality/test/cli.test.mjs +++ b/tools/quality/test/cli.test.mjs @@ -40,7 +40,8 @@ function fixture() { assert.equal(result.status, 0, result.stderr); return result.stdout.trim(); }; - git('init', '-b', 'main'); + git('init'); + git('symbolic-ref', 'HEAD', 'refs/heads/main'); git('config', 'user.email', 'quality@example.invalid'); git('config', 'user.name', 'Quality fixture'); const commit = () => { diff --git a/tools/quality/test/git-portability.test.mjs b/tools/quality/test/git-portability.test.mjs new file mode 100644 index 0000000..832c379 --- /dev/null +++ b/tools/quality/test/git-portability.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +/** Re-run both real Git fixture families under an old-Git command boundary; no successful Git operation is substituted. */ +test('Git fixtures retain real history when git init does not support branch flags', () => { + const cwd = mkdtempSync(join(tmpdir(), 'quality-old-git-')); + const log = join(cwd, 'git-calls.jsonl'); + const shim = join(cwd, 'reject-new-git-flags.mjs'); + // A Node preload avoids Unix executable/shebang assumptions and affects only this test's child processes. + writeFileSync( + shim, + ` +import childProcess from 'node:child_process'; +import { appendFileSync } from 'node:fs'; +import { syncBuiltinESMExports } from 'node:module'; +const actualSpawnSync = childProcess.spawnSync; +childProcess.spawnSync = function(command, args = [], options) { + if (command === 'git') { + appendFileSync(process.env.QUALITY_PORTABILITY_LOG, JSON.stringify(args) + '\\n'); + if (args[0] === 'init' && args.some((arg) => arg.startsWith('-b') || arg === '--initial-branch' || arg.startsWith('--initial-branch='))) { + return actualSpawnSync(process.execPath, ['-e', 'process.stderr.write("fixture: git init branch flags are unavailable\\\\n"); process.exit(97)'], options); + } + } + return actualSpawnSync(command, args, options); +}; +syncBuiltinESMExports(); +`, + ); + const env = { + ...process.env, + NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${pathToFileURL(shim).href}`, + QUALITY_PORTABILITY_LOG: log, + }; + // This is an independent runner, not a child shard controlled by the outer Node test harness. + delete env.NODE_TEST_CONTEXT; + try { + // Prove the child-process boundary actually rejects the formerly required invocation. + const unsupported = spawnSync( + process.execPath, + [ + '--input-type=module', + '-e', + ` + import { spawnSync } from 'node:child_process'; + const result = spawnSync('git', ['init', '-b', 'main'], {encoding:'utf8'}); + process.stderr.write(result.stderr ?? ''); + process.exit(result.status ?? 1); + `, + ], + { cwd, env, encoding: 'utf8' }, + ); + assert.equal(unsupported.status, 97, unsupported.stderr); + assert.match(unsupported.stderr, /branch flags are unavailable/); + const template = fileURLToPath(new URL('..', import.meta.url)); + const result = spawnSync( + process.execPath, + [ + '--test', + '--test-reporter=tap', + '--test-name-pattern=real CLI blocks undocumented|actual guards require all current docs', + join(template, 'test/cli.test.mjs'), + join(template, 'test/guard-history.test.mjs'), + ], + { + cwd: template, + env, + encoding: 'utf8', + timeout: 60_000, + maxBuffer: 8 * 1024 * 1024, + }, + ); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.match(result.stdout, /# pass 2\b/); + assert.match(result.stdout, /# fail 0\b/); + const calls = readFileSync(log, 'utf8') + .trim() + .split('\n') + .map((line) => JSON.parse(line)); + assert.equal( + calls.filter((args) => JSON.stringify(args) === '["init"]').length, + 2, + ); + assert.equal( + calls.filter( + (args) => + JSON.stringify(args) === '["symbolic-ref","HEAD","refs/heads/main"]', + ).length, + 2, + ); + assert.ok(calls.some((args) => args[0] === 'commit')); + assert.ok(calls.some((args) => args[0] === 'show')); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); diff --git a/tools/quality/test/guard-history.test.mjs b/tools/quality/test/guard-history.test.mjs index b49ffa7..390086e 100644 --- a/tools/quality/test/guard-history.test.mjs +++ b/tools/quality/test/guard-history.test.mjs @@ -27,7 +27,8 @@ function fixture() { assert.equal(result.status, 0, result.stderr); return result.stdout.trim(); }; - git('init', '-b', 'main'); + git('init'); + git('symbolic-ref', 'HEAD', 'refs/heads/main'); git('config', 'user.name', 'Quality fixture'); git('config', 'user.email', 'quality@example.invalid'); return { From 0c162b5e6e2c8e2a8561dc231605ddaaf74e0f95 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 08:58:34 +0500 Subject: [PATCH 08/11] chore(quality): enforce authored extension coverage in hooks and CI --- .dependency-cruiser.cjs | 4 +- AGENTS.md | 2 + eslint.complexity.config.mjs | 1 + eslint.config.mjs | 1 + eslint.typed.config.mjs | 1 + knip.json | 14 +- package.json | 15 +- scripts/quality-extensions.test.mjs | 101 ++++++++++++++ scripts/quality-hook.test.mjs | 205 ++++++++++++++++++++++++++++ tsconfig.quality.json | 20 +-- 10 files changed, 346 insertions(+), 18 deletions(-) create mode 100644 scripts/quality-extensions.test.mjs create mode 100644 scripts/quality-hook.test.mjs diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index c0b6d15..1459456 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -33,7 +33,7 @@ module.exports = { name: 'no-sdk-in-pure-transforms', severity: 'error', from: { - path: '^src/(?:auth|json-schema-to-zod|logging|rate-limit)\\.ts$', + path: '^src/(?:auth|json-schema-to-zod|logging|rate-limit)\\.(?:ts|tsx|mts|cts)$', }, to: { path: 'node_modules/(?:@1msg|express|@modelcontextprotocol)/', @@ -51,7 +51,7 @@ module.exports = { enhancedResolveOptions: { exportsFields: ['exports'], conditionNames: ['require', 'node', 'default'], - extensions: ['.js', '.ts', '.json'], + extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.json'], }, exclude: '(?:^|/)node_modules/', }, diff --git a/AGENTS.md b/AGENTS.md index 6e5e0b0..d11302c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,3 +11,5 @@ Historical debt is recorded per finding in `quality/baselines`, with adoption SH The `quality-required` job fails on failed/skipped/cancelled prerequisites. Configure branch protection to require it; YAML alone does not change server protection. Tests use isolated/local inputs; never point them at production services. Generated MCP files are owned by the pinned API producer. Run `npm run test:generated` to replay the exact producer against the checksummed public contract and locked SDK declarations, including changed/deleted/ignored-output negative fixtures. Update the producer provenance before deliberately running `node quality/mcp/generate.mjs --write`; never hand-edit the two generated files. + +The strict source program excludes only `quality/mcp/vendor/apis/**`: these incomplete API snapshots are checksummed generator inputs, not executable modules. `test:generated` verifies their exact provenance; generated runtime outputs remain in the strict program. All authored TS/TSX/MTS/CTS files remain covered. diff --git a/eslint.complexity.config.mjs b/eslint.complexity.config.mjs index 2b03820..f25b4b8 100644 --- a/eslint.complexity.config.mjs +++ b/eslint.complexity.config.mjs @@ -1,5 +1,6 @@ import { maintainabilityConfig, tseslint } from './tools/quality/eslint.mjs'; export default [ + { files: ['**/*.jsx'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, { linterOptions: { reportUnusedDisableDirectives: 'off' } }, { ignores: [ diff --git a/eslint.config.mjs b/eslint.config.mjs index 23adb8f..56b9ebb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ import { staticConfig, globals } from './tools/quality/eslint.mjs'; export default [ + { files: ['**/*.jsx'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, { ignores: [ '**/node_modules/**', diff --git a/eslint.typed.config.mjs b/eslint.typed.config.mjs index 19b53ed..d106a59 100644 --- a/eslint.typed.config.mjs +++ b/eslint.typed.config.mjs @@ -6,6 +6,7 @@ export default [ '**/dist/**', '**/tools/quality/**', '**/quality/reports/**', + 'quality/mcp/vendor/apis/**', 'src/tools.generated.ts', 'src/handlers.generated.ts', ], diff --git a/knip.json b/knip.json index f87ea47..ac7b7e2 100644 --- a/knip.json +++ b/knip.json @@ -2,13 +2,21 @@ "entry": [ "src/index.ts!", "src/http.ts!", - "scripts/*.{js,mjs,cjs}", + "scripts/pack-smoke.mjs", + "scripts/quality-extensions.test.mjs", + "scripts/quality-hook.test.mjs", + "scripts/quality-negative.mjs", + "scripts/smoke-tools-list.js", "jest.config.js", "eslint*.mjs", "quality.config.mjs", "quality/mcp/generate.mjs!", - "quality/mcp/verify-negative.mjs!" + "quality/mcp/verify-negative.mjs" + ], + "project": [ + "src/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}!", + "scripts/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}", + "quality/mcp/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}!" ], - "project": ["src/**/*.ts!", "scripts/*.{js,mjs,cjs}", "quality/mcp/*.mjs!"], "ignore": ["tools/quality/**"] } diff --git a/package.json b/package.json index b9136b4..c857a64 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,13 @@ "prepublishOnly": "npm run build", "typecheck": "tsc -p tsconfig.quality.json", "lint:static": "node tools/quality/node_modules/eslint/bin/eslint.js . --max-warnings=0", - "lint:typed": "node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.typed.config.mjs src --max-warnings=0", + "lint:typed": "node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.typed.config.mjs . --max-warnings=0", "lint:unused:production": "node tools/quality/run.mjs unused-production", "lint:unused:full": "node tools/quality/run.mjs unused-full", "lint:duplicates": "node tools/quality/run.mjs duplicates", "quality:guards": "node tools/quality/run.mjs guards", "lint:arch": "node tools/quality/node_modules/dependency-cruiser/bin/dependency-cruiser.mjs --config .dependency-cruiser.cjs src", - "test:quality": "node scripts/quality-negative.mjs src", + "test:quality": "node scripts/quality-negative.mjs src && node --test --test-concurrency=1 scripts/quality-hook.test.mjs scripts/quality-extensions.test.mjs", "lint:complexity": "node tools/quality/run.mjs report --kind eslint --name complexity -- node tools/quality/node_modules/eslint/bin/eslint.js -c eslint.complexity.config.mjs . --format json", "lint:duplicates:full": "node tools/quality/run.mjs duplicates-full", "format:check": "node tools/quality/node_modules/prettier/bin/prettier.cjs --check .", @@ -73,6 +73,15 @@ }, "packageManager": "npm@11.17.0", "lint-staged": { - "*.{js,ts,mjs,cjs,json,md,yml,yaml}": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write" + "*.{js,jsx,mjs,cjs}": [ + "node tools/quality/node_modules/prettier/bin/prettier.cjs --write", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0" + ], + "*.{ts,tsx,mts,cts}": [ + "node tools/quality/node_modules/prettier/bin/prettier.cjs --write", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.typed.config.mjs --max-warnings=0" + ], + "*.{json,md,yml,yaml}": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write" } } diff --git a/scripts/quality-extensions.test.mjs b/scripts/quality-extensions.test.mjs new file mode 100644 index 0000000..5779068 --- /dev/null +++ b/scripts/quality-extensions.test.mjs @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import test from 'node:test'; + +const sourceRoot = 'src'; +const runtimeFile = 'src/__quality_extension_runtime.ts'; +const testFile = 'src/__quality_extension_runtime.test.ts'; + +/** Read actual Knip file diagnostics; a malformed report or tool failure cannot masquerade as coverage. */ +function unusedFiles(production) { + const result = spawnSync( + process.execPath, + [ + 'tools/quality/node_modules/knip/bin/knip.js', + '--config', + 'knip.json', + '--reporter', + 'json', + '--no-config-hints', + '--no-progress', + '--include', + 'files', + ...(production ? ['--production'] : []), + ], + { encoding: 'utf8', timeout: 60_000, maxBuffer: 16 * 1024 * 1024 }, + ); + assert.equal(result.signal, null, String(result.error ?? result.stderr)); + assert.equal( + result.status, + 1, + `Unused probes must fail Knip: ${result.stdout}\n${result.stderr}`, + ); + const report = JSON.parse(result.stdout); + assert.ok(Array.isArray(report.issues), result.stdout); + const files = new Set(); + for (const issue of report.issues) for (const entry of issue.files ?? []) files.add(entry.name); + return files; +} + +/** Create an isolated test dependency without changing existing entry files or executing the candidate modules. */ +function createTestDependency() { + for (const file of [runtimeFile, testFile]) { + assert.equal(existsSync(file), false, `Refusing to overwrite ${file}`); + mkdirSync(dirname(file), { recursive: true }); + } + const target = './' + relative(dirname(testFile), runtimeFile).replaceAll('\\', '/'); + if (runtimeFile.endsWith('.cjs')) { + writeFileSync(runtimeFile, 'module.exports = { onlyInTests: 1 };\n'); + writeFileSync( + testFile, + `const { onlyInTests } = require('${target}');\nconsole.log(onlyInTests);\n`, + ); + } else { + writeFileSync(runtimeFile, 'export const onlyInTests = 1;\n'); + writeFileSync( + testFile, + `import { onlyInTests } from '${target}';\nconsole.log(onlyInTests);\n`, + ); + } +} + +/** New extensions enter both inventories, while a runtime module reachable only from a test remains unused in production. */ +test('Knip inventories authored extensions without treating tests as production entries', () => { + const directory = join(sourceRoot, '__quality_extension_inventory__'); + assert.equal(existsSync(directory), false, `Refusing to overwrite ${directory}`); + assert.equal(existsSync(runtimeFile), false, `Refusing to overwrite ${runtimeFile}`); + assert.equal(existsSync(testFile), false, `Refusing to overwrite ${testFile}`); + mkdirSync(directory); + const files = ['js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'mts', 'cts'].map((extension) => + join(directory, `unused-${extension}.${extension}`), + ); + try { + for (const file of files) + writeFileSync( + file, + file.endsWith('.cjs') + ? 'module.exports = { unusedExtensionProbe: 1 };\n' + : 'export const unusedExtensionProbe = 1;\n', + ); + createTestDependency(); + for (const production of [false, true]) { + const reported = unusedFiles(production); + for (const file of files) + assert.ok( + reported.has(file.replaceAll('\\', '/')), + `${production ? 'production' : 'full'} omitted ${file}`, + ); + assert.equal( + reported.has(runtimeFile), + production, + `Test-only runtime dependency ${runtimeFile} must be reachable only in the full graph`, + ); + } + } finally { + rmSync(directory, { recursive: true, force: true }); + rmSync(runtimeFile, { force: true }); + rmSync(testFile, { force: true }); + } +}); diff --git a/scripts/quality-hook.test.mjs b/scripts/quality-hook.test.mjs new file mode 100644 index 0000000..55866d4 --- /dev/null +++ b/scripts/quality-hook.test.mjs @@ -0,0 +1,205 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { + chmodSync, + statSync, + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import test from 'node:test'; + +const repository = resolve('.'); +const configuration = { + source: 'src', + tooling: 'scripts', + typed: true, + files: [ + 'package.json', + '.husky/pre-commit', + 'eslint.config.mjs', + 'eslint.static.config.mjs', + 'eslint.typed.config.mjs', + 'eslint.hook.config.mjs', + 'tsconfig.json', + 'tsconfig.quality.json', + 'prettier.config.mjs', + '.prettierrc', + '.prettierrc.json', + '.prettierrc.js', + '.prettierignore', + ], + links: ['node_modules', 'tools/quality'], +}; + +/** Run the real hook tools with an isolated Git identity and bounded child lifetime. */ +function command(root, program, args) { + const env = { + ...process.env, + HUSKY: '1', + GIT_CONFIG_NOSYSTEM: '1', + GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null', + }; + for (const key of Object.keys(env)) + if (key.startsWith('GIT_') && !['GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL'].includes(key)) + delete env[key]; + const result = spawnSync(program, args, { + cwd: root, + env, + encoding: 'utf8', + timeout: 60_000, + maxBuffer: 8 * 1024 * 1024, + }); + assert.equal( + result.signal, + null, + `Fixture must finish by diagnostics: ${result.error ?? result.stderr}`, + ); + return result; +} + +/** Preserve full diagnostics when setup or a corrected commit unexpectedly fails. */ +function successful(root, program, args) { + const result = command(root, program, args); + assert.equal( + result.status, + 0, + `${program} ${args.join(' ')}\n${result.stdout}\n${result.stderr}`, + ); + return result.stdout.trim(); +} + +/** Copy production configuration and install the actual executable hook without borrowing the user's Git settings. */ +function fixture(t) { + const root = mkdtempSync(join(tmpdir(), '1msg-hook-extensions-')); + /** Remove only this disposable checkout after all real commit attempts finish. */ + t.after(() => rmSync(root, { recursive: true, force: true })); + for (const file of configuration.files) { + if (!existsSync(join(repository, file))) continue; + mkdirSync(dirname(join(root, file)), { recursive: true }); + copyFileSync(join(repository, file), join(root, file)); + } + for (const directory of configuration.links) { + mkdirSync(dirname(join(root, directory)), { recursive: true }); + symlinkSync(join(repository, directory), join(root, directory), 'dir'); + } + const actualPackage = JSON.parse(readFileSync(join(repository, 'package.json'), 'utf8')); + writeFileSync(join(root, 'package.json'), JSON.stringify(actualPackage)); + successful(root, 'git', ['init', '--quiet']); + successful(root, 'git', ['config', 'user.name', 'Quality fixture']); + successful(root, 'git', ['config', 'user.email', 'quality-fixture@example.invalid']); + successful(root, 'git', [ + '-c', + 'commit.gpgsign=false', + 'commit', + '--quiet', + '--allow-empty', + '-m', + 'fixture setup', + ]); + chmodSync(join(root, '.husky/pre-commit'), 0o755); + successful(root, process.execPath, ['tools/quality/node_modules/husky/bin.js']); + assert.equal(successful(root, 'git', ['config', '--local', 'core.hooksPath']), '.husky/_'); + assert.ok(statSync(join(root, '.husky/_/pre-commit')).mode & 0o111); + return root; +} + +/** Stage normalized fixture text so the intended lint rule, rather than unrelated formatting, refuses the commit. */ +function stage(root, files, source) { + for (const file of files) { + mkdirSync(dirname(join(root, file)), { recursive: true }); + writeFileSync(join(root, file), source); + } + successful(root, process.execPath, [ + 'tools/quality/node_modules/prettier/bin/prettier.cjs', + '--write', + ...files, + ]); + successful(root, 'git', ['add', '--', ...files]); +} + +/** A rejected commit must retain HEAD and identify every tested extension, preventing one covered file from masking an ignored peer. */ +function rejected(root, files, diagnostic) { + const head = successful(root, 'git', ['rev-parse', 'HEAD']); + const result = command(root, 'git', [ + '-c', + 'commit.gpgsign=false', + 'commit', + '--quiet', + '-m', + 'reject invalid authored extensions', + ]); + const output = `${result.stdout}\n${result.stderr}`.replaceAll('\\', '/'); + assert.notEqual(result.status, 0, output); + assert.match(output, diagnostic); + for (const file of files) { + const sections = output.split(/\n\s*\n/).filter((section) => section.includes(file)); + assert.ok( + sections.some((section) => diagnostic.test(section)), + `Missing intended diagnostic for ${file}:\n${output}`, + ); + } + assert.equal(successful(root, 'git', ['rev-parse', 'HEAD']), head); + successful(root, 'git', ['reset', '--quiet', 'HEAD', '--', ...files]); + for (const file of files) rmSync(join(root, file)); +} + +/** Exercise actual commits for all authored extensions; strict TS boundaries additionally require their configured program. */ +test('installed pre-commit covers JavaScript and TypeScript extensions', (t) => { + const root = fixture(t); + /** Keep fixture paths inside the actual source program and give each extension its own diagnostic. */ + const paths = (extensions) => + extensions.map((extension) => `${configuration.source}/hook-${extension}.${extension}`); + const javascript = paths(['js', 'jsx', 'mjs', 'cjs']); + const typescript = paths(['ts', 'tsx', 'mts', 'cts']); + stage( + root, + javascript, + 'const value = process.env.HOOK_FIXTURE_VALUE;\nconsole.log(value == 1);\n', + ); + rejected(root, javascript, /eqeqeq/); + stage( + root, + typescript, + '/** Preserve the supplied untrusted value. */\nexport function same(value: any) { return value; }\n', + ); + rejected( + root, + typescript, + configuration.typed ? /no-explicit-any/ : /TypeScript requires a configured strict program/, + ); + if (configuration.typed) { + const boundaries = [...typescript, `${configuration.tooling}/hook-tooling.mts`]; + stage( + root, + boundaries, + '/** Decode external text without trusting its shape. */\nexport function decode(text: string): string { return JSON.parse(text); }\n', + ); + rejected(root, boundaries, /no-unsafe-return/); + stage( + root, + boundaries, + '/** Return the supplied typed value unchanged. */\nexport function identity(value: string): string { return value; }\n', + ); + } + stage( + root, + javascript, + 'const value = process.env.HOOK_FIXTURE_VALUE;\nconsole.log(value === 1);\n', + ); + successful(root, 'git', [ + '-c', + 'commit.gpgsign=false', + 'commit', + '--quiet', + '-m', + 'accept corrected authored extensions', + ]); +}); diff --git a/tsconfig.quality.json b/tsconfig.quality.json index 946964a..9269464 100644 --- a/tsconfig.quality.json +++ b/tsconfig.quality.json @@ -5,15 +5,15 @@ "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noEmitOnError": true, - "rootDir": "." + "rootDir": ".", + "jsx": "preserve" }, - "include": [ - "src/**/*.ts", - "tests/**/*.ts", - "vitest.config.ts", - "nodes/**/*.ts", - "utils/**/*.ts", - "credentials/**/*.ts" - ], - "exclude": ["node_modules", "dist"] + "include": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], + "exclude": [ + "**/node_modules/**", + "**/dist/**", + "tools/quality/**", + "quality/reports/**", + "quality/mcp/vendor/apis/**" + ] } From e0781c0d56372157818ba98684f4e33ecd34610b Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 09:26:29 +0500 Subject: [PATCH 09/11] test(mcp): use strict compiler settings and require natural exit --- AGENTS.md | 2 ++ jest.config.js | 2 +- tsconfig.quality.json | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d11302c..794535c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,3 +13,5 @@ The `quality-required` job fails on failed/skipped/cancelled prerequisites. Conf Generated MCP files are owned by the pinned API producer. Run `npm run test:generated` to replay the exact producer against the checksummed public contract and locked SDK declarations, including changed/deleted/ignored-output negative fixtures. Update the producer provenance before deliberately running `node quality/mcp/generate.mjs --write`; never hand-edit the two generated files. The strict source program excludes only `quality/mcp/vendor/apis/**`: these incomplete API snapshots are checksummed generator inputs, not executable modules. `test:generated` verifies their exact provenance; generated runtime outputs remain in the strict program. All authored TS/TSX/MTS/CTS files remain covered. + +Jest uses the strict quality TypeScript program with isolated module compilation for NodeNext. Tests must release their listeners/transports and terminate naturally; forced process exit is not a substitute for cleanup. diff --git a/jest.config.js b/jest.config.js index ffbb9cf..e06f3be 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ /** @type {import('jest').Config} */ module.exports = { preset: 'ts-jest', + transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.quality.json' }] }, testEnvironment: 'node', roots: ['/src'], testMatch: ['**/__tests__/**/*.test.ts'], @@ -8,5 +9,4 @@ module.exports = { testTimeout: 15000, // zod@4 + parallel workers can flake on module resolution; keep CI deterministic maxWorkers: 1, - forceExit: true, }; diff --git a/tsconfig.quality.json b/tsconfig.quality.json index 9269464..e5c3a6b 100644 --- a/tsconfig.quality.json +++ b/tsconfig.quality.json @@ -6,7 +6,8 @@ "exactOptionalPropertyTypes": true, "noEmitOnError": true, "rootDir": ".", - "jsx": "preserve" + "jsx": "preserve", + "isolatedModules": true }, "include": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], "exclude": [ From 2ced9956958af85d0fd371b1202cd6d4bbaa80a1 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Tue, 13 Oct 2026 12:08:39 +0500 Subject: [PATCH 10/11] fix(quality): parse suppression comments with template context --- package.json | 8 +- scripts/quality-hook.test.mjs | 1 + tools/quality/README.md | 20 ++++ tools/quality/lib/guards.mjs | 47 +++++++-- tools/quality/package-lock.json | 4 +- tools/quality/package.json | 2 +- tools/quality/staged.mjs | 43 ++++++++ tools/quality/template-manifest.json | 14 +-- tools/quality/test/cli.test.mjs | 74 ++++++++++++++ .../quality/test/suppression-syntax.test.mjs | 97 +++++++++++++++++++ 10 files changed, 288 insertions(+), 22 deletions(-) create mode 100644 tools/quality/staged.mjs create mode 100644 tools/quality/test/suppression-syntax.test.mjs diff --git a/package.json b/package.json index c857a64..d14bd6d 100644 --- a/package.json +++ b/package.json @@ -74,13 +74,15 @@ "packageManager": "npm@11.17.0", "lint-staged": { "*.{js,jsx,mjs,cjs}": [ + "node tools/quality/staged.mjs --config eslint.config.mjs --", "node tools/quality/node_modules/prettier/bin/prettier.cjs --write", - "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0" + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0 --no-warn-ignored" ], "*.{ts,tsx,mts,cts}": [ + "node tools/quality/staged.mjs --config eslint.config.mjs --", "node tools/quality/node_modules/prettier/bin/prettier.cjs --write", - "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0", - "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.typed.config.mjs --max-warnings=0" + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.config.mjs --max-warnings=0 --no-warn-ignored", + "node tools/quality/node_modules/eslint/bin/eslint.js --config eslint.typed.config.mjs --max-warnings=0 --no-warn-ignored" ], "*.{json,md,yml,yaml}": "node tools/quality/node_modules/prettier/bin/prettier.cjs --write" } diff --git a/scripts/quality-hook.test.mjs b/scripts/quality-hook.test.mjs index 55866d4..49f8328 100644 --- a/scripts/quality-hook.test.mjs +++ b/scripts/quality-hook.test.mjs @@ -23,6 +23,7 @@ const configuration = { typed: true, files: [ 'package.json', + 'quality.config.mjs', '.husky/pre-commit', 'eslint.config.mjs', 'eslint.static.config.mjs', diff --git a/tools/quality/README.md b/tools/quality/README.md index 9c78337..750bd5c 100644 --- a/tools/quality/README.md +++ b/tools/quality/README.md @@ -150,3 +150,23 @@ not modify the runtime Git driver or skip checks on Windows. Only fixture setup changes: analysis rules, historical identities and baseline records are unchanged. Copy the complete sealed version without changing debt owners, reasons, issue links, expiry dates or baseline entries. + +## 1.0.12 migration + +Suppression detection now scans trivia between parsed TypeScript tokens. A plain +scanner loses context after a template interpolation: it can mistake generated +directive text for a source comment and overlook a real directive after the +template. Parsed template segments, regex bodies and JSX text remain data; +comments inside interpolations/JSX expressions and at EOF remain enforceable. +Positive and negative syntax fixtures and the real guard CLI cover both failure +modes, including scoped exception requirements and Svelte script blocks. +Copy the complete sealed version. Metric identities, source coverage and debt +allowances do not change; rerun guards to detect any previously missed comments. + +The staged-file coverage preflight (`staged.mjs --config -- +`) permits ignored files only when the repository inventory explicitly +declares them or they belong to the verified sealed preset. Run it before ESLint +with `--no-warn-ignored`: this supports preset updates and generated files while +still rejecting an accidentally ignored authored file. An added or altered +unsealed vendor file fails integrity validation; an arbitrary ESLint ignore alone +never grants an exemption. diff --git a/tools/quality/lib/guards.mjs b/tools/quality/lib/guards.mjs index 69e3bde..0e3fddb 100644 --- a/tools/quality/lib/guards.mjs +++ b/tools/quality/lib/guards.mjs @@ -280,29 +280,56 @@ function historicalFunctions(path, text) { } } -/** Identify suppression directives in comments only, never in strings or generated documentation examples. */ -export function suppressionErrors(path, text, config) { - const errors = []; - for (const { ast } of parseUnits(path, text)) { +/** Yield parsed lexical leaves without descending into documentation or splitting literal/JSX content. */ +function* syntaxTokens(node, ast) { + if ( + (node.kind >= ts.SyntaxKind.FirstJSDocNode && + node.kind <= ts.SyntaxKind.LastJSDocNode) || + node.kind === ts.SyntaxKind.JSDocComment + ) + return; + const children = node.getChildren(ast); + if (children.length) { + for (const child of children) yield* syntaxTokens(child, ast); + } else if (node.kind !== ts.SyntaxKind.SyntaxList) { + yield node; + } +} + +/** Scan only trivia between parsed tokens; interpolation expressions retain comments while template/regex/JSX bodies stay opaque. */ +function* sourceComments(ast) { + let end = 0; + for (const token of syntaxTokens(ast, ast)) { + const start = token.getStart(ast); const scanner = ts.createScanner( ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, - ast.text, + ast.text.slice(end, start), ); for ( - let token = scanner.scan(); - token !== ts.SyntaxKind.EndOfFileToken; - token = scanner.scan() + let trivia = scanner.scan(); + trivia !== ts.SyntaxKind.EndOfFileToken; + trivia = scanner.scan() ) { if ( ![ ts.SyntaxKind.SingleLineCommentTrivia, ts.SyntaxKind.MultiLineCommentTrivia, - ].includes(token) + ].includes(trivia) ) continue; - const comment = scanner.getTokenText(); + yield scanner.getTokenText(); + } + end = token.end; + } +} + +/** Identify suppression directives in comments only, never in strings or generated documentation examples. */ +export function suppressionErrors(path, text, config) { + const errors = []; + for (const { ast } of parseUnits(path, text)) { + for (const comment of sourceComments(ast)) { if ( /@ts-(?:ignore|nocheck)\b/.test(comment) || /eslint-disable(?!-next-line|-line)\b/.test(comment) diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json index 527d33a..bf75285 100644 --- a/tools/quality/package-lock.json +++ b/tools/quality/package-lock.json @@ -1,12 +1,12 @@ { "name": "@1msg/quality-template", - "version": "1.0.11", + "version": "1.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@1msg/quality-template", - "version": "1.0.11", + "version": "1.0.12", "dependencies": { "@eslint/js": "9.39.5", "dependency-cruiser": "18.6.0", diff --git a/tools/quality/package.json b/tools/quality/package.json index 67942c0..f6bbd11 100644 --- a/tools/quality/package.json +++ b/tools/quality/package.json @@ -1,6 +1,6 @@ { "name": "@1msg/quality-template", - "version": "1.0.11", + "version": "1.0.12", "private": true, "type": "module", "engines": { diff --git a/tools/quality/staged.mjs b/tools/quality/staged.mjs new file mode 100644 index 0000000..bbfa798 --- /dev/null +++ b/tools/quality/staged.mjs @@ -0,0 +1,43 @@ +import { ESLint } from 'eslint'; +import { readFileSync, existsSync } from 'node:fs'; +import { relative, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { matches } from './lib/guards.mjs'; +import { verifyTemplate } from './lib/template.mjs'; + +const [, , flag, configuration, separator, ...files] = process.argv; +if (flag !== '--config' || !configuration || separator !== '--') + throw new Error('Usage: staged.mjs --config -- '); + +const { default: inventory } = await import( + pathToFileURL(resolve('quality.config.mjs')).href +); +const approved = [ + ...(inventory.generated ?? []), + ...(inventory.excluded ?? []), +].map((entry) => entry.pattern); +const manifestPath = 'tools/quality/template-manifest.json'; +let vendorFiles = new Set(); +if (existsSync(manifestPath)) { + verifyTemplate('HEAD'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + vendorFiles = new Set([ + manifestPath, + ...Object.keys(manifest.files).map((path) => `tools/quality/${path}`), + ]); +} +const eslint = new ESLint({ overrideConfigFile: configuration }); +const uncovered = []; +for (const file of files) { + const path = relative(process.cwd(), resolve(file)).replaceAll('\\', '/'); + if ( + (await eslint.isPathIgnored(file)) && + !vendorFiles.has(path) && + !matches(path, approved) + ) + uncovered.push(path); +} +if (uncovered.length) + throw new Error( + `Staged authored files have no ESLint coverage:\n${uncovered.join('\n')}`, + ); diff --git a/tools/quality/template-manifest.json b/tools/quality/template-manifest.json index 210f229..4e79096 100644 --- a/tools/quality/template-manifest.json +++ b/tools/quality/template-manifest.json @@ -1,29 +1,31 @@ { "schemaVersion": 1, - "version": "1.0.11", + "version": "1.0.12", "files": { - "README.md": "6760e252841ba82078ede1589a474dd19c8f77f828245e181f34e842465c3aa1", + "README.md": "80380757947a33100e4f43f7808b129f2c3cdc81c380614fc2e55153ef5f124a", "canonical-duplicates.mjs": "a3583fdc3e344af448ea8d2543c2398daaa03fa10a0739da486afb04e35425ba", "complexity.mjs": "4ed5553c392621e0615a48c12e12300c8602ed2a28cfbaff67002acf82e49cac", "eslint.mjs": "607ddfaac3d365fedd7dbc5e21c781d448347b1c36662f3492e4364ff9f22ded", "lib/baseline.mjs": "7876ba90b80448d9c9003c21e348095ed22630c41fc26ed944984742020549dd", "lib/git.mjs": "b7c26871526d864bf8d12f01c613d3bfa0adb2da9d072544d8f09407ef598758", - "lib/guards.mjs": "b1b025a1fdcb63434733216a4faaa45112aa404f26f9d31df28921243d6a22f8", + "lib/guards.mjs": "fe95129ef78aa5354446d8b37769ac27deb95d2b8bc978c5d3bafad538bd7836", "lib/historical.mjs": "43b46395828daa3f19d8692e9bd22ea6cfc1d0dbac83814d727894f672a0ddd2", "lib/knip-reporter.mjs": "175d7d96e2913e8f6de04b6c706172258d9d7654a26ba7f9ed7edff79d3fc8ca", "lib/reports.mjs": "361e38ae981238db91f7b9a31af963ccbb89e3ae6745d1582b2086e6c64f0c9e", "lib/template.mjs": "c2a14b715f0c9dbe8b8243b142f4c6628967400fb8f92767537169e1b7bfd8f4", - "package-lock.json": "9fc7d279d2bade1c42f807d99a107d77f15a8e42edd2a9b32b4b5ce1a4fda35f", - "package.json": "deb5fd5e972aec2926b21e31d02c47e9c35d507048e83ab0ed161425151546e2", + "package-lock.json": "238e6a8ff9303c66968b8e83ef028dfa49b7bb6a5ca485c0a7e65520d4c54365", + "package.json": "91adacf496715638a7e765035ca16d27e7a3c40ad3abbdd4bb4fc8c9574113eb", "prettier.config.mjs": "7576563a9a30aff8eb3177f9cc2e4bc7a0f6b825042eadb9a619b86f519c5e5e", "run.mjs": "7cf26128cdf63f3a7ccab7341a0a274a352fd9bda0521ba41e37216bb9cc249f", "seal.mjs": "fb38c8e08ea3cf9f4dcd95e45ffc931aea3b7a54181fed88d7379f8aad4c050d", - "test/cli.test.mjs": "49c55b1b4cf2ca2214b0f1b5d2bd973eac43005e14606f27a2a4aa682e0e691d", + "staged.mjs": "17db5c61a1369e6e740fd233887eca0033b64f2114dd3871b96108afb6123d49", + "test/cli.test.mjs": "a8e066e20790fc0d915b6a6d7ba11ca8d2af2a59ca6d51f4da232e7efb511f39", "test/complexity.test.mjs": "ba1f702ba94de3fcad93cb5e43a49bc2456be1426c81754149420d210f2b3952", "test/git-portability.test.mjs": "0e65759ff3a1aa87c20d20ae147154d980381c2e8075f57f341ef3c53c3b3f62", "test/guard-history.test.mjs": "00b17403b3ef17cc5d83c681c990ca98b6a0ad4857a95d501d2cf8f390aa7b1e", "test/ratchet.test.mjs": "324a932c5a52d1b037e111042e1b41e5686f8d4dbfd0307faf86cee649ebc27a", "test/reports.test.mjs": "521a13da2f16628b4a30de7040529e0b7b7e25e7db79d19de8004945d49c5e0c", + "test/suppression-syntax.test.mjs": "1b3dfc3a66ee9978491402e3683511c2d3a6d42203b283cd4a74578551a197b0", "tsconfig.next.json": "d900a35fcb0852376b06854df5be2f8f178e26cb5383b93b07ebeb6220e30faf", "tsconfig.node.json": "627661abcb7e992c4bfaf98d6e68a12678573b5d4f6e821ea0fa43fa6e233f28", "tsconfig.svelte.json": "5777247ea98aaf2d886b87cd12206a641377edcdd086ce5fd5b58adc6f0bc771" diff --git a/tools/quality/test/cli.test.mjs b/tools/quality/test/cli.test.mjs index 0c8d1cd..88b6aa6 100644 --- a/tools/quality/test/cli.test.mjs +++ b/tools/quality/test/cli.test.mjs @@ -99,6 +99,80 @@ test('real CLI blocks undocumented new functions, broad suppression and uncovere } }); +/** The real guard CLI distinguishes generated directive text from actual suppressions in and after interpolations. */ +test('actual guards CLI enforces comments around interpolated template data', () => { + const f = fixture(); + try { + f.write('src/index.js', 'console.log("app");\n'); + const base = f.commit(); + f.write( + 'src/index.js', + 'const generated = `a ${name} /* eslint-disable */`;\nconsole.log(generated);\n', + ); + const valid = f.run('guards', '--base', base); + assert.equal(valid.status, 0, valid.stderr); + for (const source of [ + 'const generated = `a ${ /* eslint-disable */ name } z`;\n', + 'const generated = `a ${name} z`; // @ts-nocheck\n', + ]) { + f.write('src/index.js', source); + const invalid = f.run('guards', '--base', base); + assert.equal(invalid.status, 1, invalid.stderr); + assert.match(invalid.stderr, /broad lint\/type suppression/); + } + } finally { + f.close(); + } +}); + +/** The actual staged preflight permits declared generated sources while rejecting ordinary ESLint ignores. */ +test('staged coverage rejects unowned ignores and validates sealed vendor files', () => { + const f = fixture(); + try { + f.write('src/index.js', 'console.log("app");\n'); + f.commit(); + f.write( + 'eslint.config.mjs', + 'export default [{ignores:["src/ignored.js","src/generated.js","tools/quality/**"]}];\n', + ); + f.write( + 'quality.config.mjs', + 'export default {generated:[{pattern:"src/generated.js",producer:"fixture"}]};\n', + ); + f.write('src/ignored.js', 'export const forgotten = 1;\n'); + f.write('src/generated.js', 'export const generated = 1;\n'); + const staged = fileURLToPath(new URL('../staged.mjs', import.meta.url)); + /** Invoke the installed ESLint resolver and real sealed inventory with only disposable files. */ + const check = (path) => + spawnSync( + process.execPath, + [staged, '--config', 'eslint.config.mjs', '--', path], + { cwd: f.cwd, encoding: 'utf8' }, + ); + assert.equal(check('src/index.js').status, 0); + assert.equal(check('src/generated.js').status, 0); + const ignored = check('src/ignored.js'); + assert.equal(ignored.status, 1); + assert.match( + ignored.stderr, + /Staged authored files have no ESLint coverage/, + ); + const template = fileURLToPath(new URL('..', import.meta.url)); + for (const path of [...templateFiles(template), 'template-manifest.json']) { + const destination = join(f.cwd, 'tools/quality', path); + mkdirSync(dirname(destination), { recursive: true }); + cpSync(join(template, path), destination); + } + assert.equal(check('tools/quality/lib/guards.mjs').status, 0); + f.write('tools/quality/rogue.js', 'export const bypass = 1;\n'); + const rogue = check('tools/quality/rogue.js'); + assert.equal(rogue.status, 1); + assert.match(rogue.stderr, /undeclared\/missing files/); + } finally { + f.close(); + } +}); + test('actual Knip CLI blocks an unused file and unresolved import', () => { const f = fixture(); try { diff --git a/tools/quality/test/suppression-syntax.test.mjs b/tools/quality/test/suppression-syntax.test.mjs new file mode 100644 index 0000000..4d5b0bf --- /dev/null +++ b/tools/quality/test/suppression-syntax.test.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { suppressionErrors } from '../lib/guards.mjs'; + +/** Template segments, regex bodies and JSX text are data even when they contain directive spelling. */ +test('suppression scanning preserves parsed literal boundaries', () => { + const sources = [ + [ + 'a.js', + 'const generated = `start ${name}\n/* eslint-disable */\n// @ts-nocheck\n`;', + ], + [ + 'a.ts', + 'const generated = `a ${`b ${name}\n// @ts-ignore\n`} c ${name} /* eslint-disable */`;', + ], + [ + 'a.mjs', + 'const generated = tag`head ${name}\n// @ts-nocheck\n${other} /* eslint-disable */`;', + ], + [ + 'a.ts', + String.raw`const pattern = /[/*]eslint-disable/; const other = /[//]@ts-ignore/;`, + ], + [ + 'a.tsx', + 'const view =
// @ts-ignore {name}/* eslint-disable */
;', + ], + [ + 'a.jsx', + 'const view =
{/* ordinary comment */} // @ts-ignore
;', + ], + [ + 'a.svelte', + '', + ], + ]; + for (const [path, source] of sources) { + assert.deepEqual( + suppressionErrors(path, source, {}), + [], + `${path}: ${source}`, + ); + } +}); + +/** Actual comments remain enforceable before, within and after expressions, including EOF trivia. */ +test('suppression scanning rejects comments inside template and JSX expressions', () => { + const sources = [ + ['a.js', 'const generated = `a ${ /* eslint-disable */ name } z`;'], + [ + 'a.ts', + 'const generated = `a ${`b ${ /* eslint-disable */ name } c`} d`;', + ], + ['a.mjs', 'const generated = `a ${name // @ts-ignore\n} z`;'], + ['a.cts', 'const generated = `a ${name} z`; // @ts-nocheck'], + ['a.tsx', 'const view =
{/* eslint-disable */ name}
;'], + ['a.jsx', 'const view =
;\n// @ts-ignore'], + ['a.ts', '/* eslint-disable */'], + [ + 'a.ts', + '/** @ts-ignore This comment is real documentation. */\nconst value = 1;', + ], + [ + 'a.ts', + String.raw`const pattern = /[/*]/; /* eslint-disable */ const value = 1;`, + ], + [ + 'a.svelte', + '', + ], + ]; + for (const [path, source] of sources) { + assert.equal( + suppressionErrors(path, source, {}).length, + 1, + `${path}: ${source}`, + ); + } +}); + +/** Malformed scoped comments must not gain an exemption merely by appearing in an interpolation. */ +test('suppression scanning retains scoped exception requirements', () => { + const source = 'const generated = `a ${ // eslint-disable-next-line\nname }`'; + assert.match(suppressionErrors('a.ts', source, {})[0], /must name rules/); + const negative = + 'const generated = `a ${ // @ts-expect-error: Verify an invalid primitive contract\nname }`'; + assert.match( + suppressionErrors('a.ts', negative, {})[0], + /negative type-test scope/, + ); + assert.deepEqual( + suppressionErrors('negative.test.ts', negative, { + negativeTypeTests: ['*.test.ts'], + }), + [], + ); +}); From ed42277e2cea37773ecb7fa8bb616a2e9aa896f3 Mon Sep 17 00:00:00 2001 From: Alibek Seksenali Date: Thu, 15 Oct 2026 01:59:07 +0500 Subject: [PATCH 11/11] ci: verify shipping artifacts and consumer compatibility --- .github/workflows/quality.yml | 28 +++++++++++++++++++- Dockerfile | 2 +- package-lock.json | 2 +- package.json | 2 +- scripts/consumer-smoke.sh | 29 +++++++++++++++++++++ scripts/container-smoke.sh | 48 +++++++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 scripts/consumer-smoke.sh create mode 100644 scripts/container-smoke.sh diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 7929766..51e44d1 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -43,9 +43,35 @@ jobs: name: quality-reports path: quality/reports/ if-no-files-found: error + package-consumer: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + node: ['18', '20', '22', '24'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24.19.0' + - run: npm ci --ignore-scripts + - run: npm run build + - name: Pack the actual distributable + run: mkdir -p "$RUNNER_TEMP/packed" && npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/packed" + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: bash scripts/consumer-smoke.sh "$RUNNER_TEMP"/packed/*.tgz '@1msg/mcp' + release-artifact: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - run: bash scripts/container-smoke.sh quality-required: if: always() - needs: [checks] + needs: [checks, package-consumer, release-artifact] runs-on: ubuntu-24.04 steps: - env: diff --git a/Dockerfile b/Dockerfile index e640828..ac1cc20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM node:24.19.0-alpine AS build WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm ci || npm install +RUN npm ci COPY tsconfig.json tsconfig.build.json ./ COPY src ./src RUN npm run build && npm prune --omit=dev diff --git a/package-lock.json b/package-lock.json index bbf4d8b..66f5a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "yaml": "2.9.0" }, "engines": { - "node": "24.x" + "node": ">=18.0.0" } }, "node_modules/@1msg/sdk": { diff --git a/package.json b/package.json index d14bd6d..4053c0e 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "yaml": "2.9.0" }, "engines": { - "node": "24.x" + "node": ">=18.0.0" }, "packageManager": "npm@11.17.0", "lint-staged": { diff --git a/scripts/consumer-smoke.sh b/scripts/consumer-smoke.sh new file mode 100644 index 0000000..327e386 --- /dev/null +++ b/scripts/consumer-smoke.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Install the tarball as a real consumer, without checkout dependencies or dev tools. +set -euo pipefail +artifact=$(realpath "$1") +consumer=$(mktemp -d) +trap 'rm -rf "$consumer"' EXIT +cd "$consumer" +printf '{"private":true}' > package.json +npm install --engine-strict --ignore-scripts --no-audit --no-fund "$artifact" +# Resolve every public executable or n8n registration from the installed package. +node - "$2" <<'NODE' +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const { resolve } = require('node:path'); +const name = process.argv[2]; +const root = resolve('node_modules', name); +const manifest = require(resolve(root, 'package.json')); +if (manifest.n8n) { + for (const entry of [...manifest.n8n.nodes, ...manifest.n8n.credentials]) { + assert.ok(Object.values(require(resolve(root, entry))).some(value => typeof value === 'function')); + } +} else { + const entry = typeof manifest.bin === 'string' ? manifest.bin : Object.values(manifest.bin)[0]; + const result = spawnSync(process.execPath, [resolve(root, entry), '--help'], { encoding: 'utf8', timeout: 30000 }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /1msg/i); +} +console.log(`${name}: fresh consumer installation and entrypoints passed on ${process.version}`); +NODE diff --git a/scripts/container-smoke.sh b/scripts/container-smoke.sh new file mode 100644 index 0000000..3fa94a4 --- /dev/null +++ b/scripts/container-smoke.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Probe the actual HTTP image with dummy credentials and no external network. +set -euo pipefail +name="mcp-smoke-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}" +cleanup() { + status=$? + if [ "$status" -ne 0 ]; then docker logs "$name" 2>&1 || true; fi + docker rm -f "$name" >/dev/null 2>&1 || true + exit "$status" +} +trap cleanup EXIT +docker build -t "$name" . +docker run -d --name "$name" --network none \ + -e ONE_MSG_BASE_URL=http://127.0.0.1:9 "$name" >/dev/null +for attempt in $(seq 1 30); do + if docker exec "$name" node -e 'fetch("http://127.0.0.1:3100/healthz").then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))'; then break; fi + sleep 1 +done +docker exec -i "$name" node --input-type=module <<'NODE' +import assert from 'node:assert/strict'; +const base = 'http://127.0.0.1:3100'; +assert.notEqual(process.getuid(), 0, 'Shipping image must run as a non-root user'); +for (const path of ['/healthz', '/readyz']) { + const response = await fetch(base + path); + assert.equal(response.status, 200); + assert.equal((await response.json()).ok, true); +} +const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }; +const initialize = { jsonrpc: '2.0', id: 1, method: 'initialize', params: { + protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'isolated-image-smoke', version: '1.0.0' } +}}; +const rejected = await fetch(base + '/mcp', { method: 'POST', headers, body: JSON.stringify(initialize) }); +assert.equal(rejected.status, 401); +headers.authorization = 'Bearer dummy-isolated-smoke-token'; +headers['x-instance-id'] = 'ODI371267300'; +const initialized = await fetch(base + '/mcp', { method: 'POST', headers, body: JSON.stringify(initialize) }); +assert.equal(initialized.status, 200); +assert.ok(initialized.headers.get('mcp-session-id')); +await initialized.text(); +headers['mcp-session-id'] = '00000000-0000-4000-8000-000000000000'; +const response = await fetch(base + '/mcp', { method: 'POST', headers, + body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }) }); +assert.equal(response.status, 200); +const body = await response.text(); +const payload = body.startsWith('{') ? JSON.parse(body) : JSON.parse(body.split('\n').find(line => line.startsWith('data:')).slice(5)); +assert.ok(payload.result.tools.length > 0); +console.log('Shipping image: non-root startup, health, readiness, auth rejection, initialize and stale-session tools/list passed'); +NODE