diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..38c8c79 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,28 @@ +module.exports = { + root: true, + env: { + es2022: true, + node: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['@typescript-eslint'], + extends: ['prettier'], + ignorePatterns: [ + 'dist/', + 'node_modules/', + 'coverage/', + 'reports/', + '.stryker-tmp/', + ], + rules: {}, + overrides: [ + { + files: ['*.mjs'], + parser: 'espree', + }, + ], +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d0fd92a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * 1' + pull_request: + push: + branches: + - main + - master + +jobs: + verify: + name: Verify on Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [16.13.0, 18.x, 20.x] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Verify + run: npm run verify + + mutation: + name: Mutation tests + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run mutation tests + run: npm run test:mutation + + audit: + name: Dependency audit + runs-on: ubuntu-latest + continue-on-error: true + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Audit dependencies + run: npm audit --audit-level=high diff --git a/.gitignore b/.gitignore index 39d7a9d..b733854 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,13 @@ node_modules /dist .DS_Store .env -/test +/test-output /temp /test-repro .catpaw .claude/settings.local.json chrome-test-1 docs/* -tests/* +coverage/ +.stryker-tmp/ +reports/mutation/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..439f87a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,51 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +## Project Overview + +PinMe is a zero-config CLI tool for deploying static sites to IPFS. Built with TypeScript, bundled with esbuild, published to npm as `pinme`. Users run commands like `pinme upload dist` to deploy frontends. + +## Build & Dev + +```bash +npm run build # Production build (esbuild → dist/index.js) +npm run dev # Dev build +npm run test # Unit/integration tests (Vitest) +npm run test:cli # Real CLI black-box tests against dist/index.js +npm run verify # Full PR gate: lint, typecheck, tests, build, CLI, pack +npm run test:mutation # Slow mutation tests (manual/nightly) +``` + +Build uses `build.js` (esbuild), NOT `rollup.config.js` (legacy, unused). esbuild reads `.env` via dotenv at build time and injects env vars as `process.env.*` defines. + +The output is a single CJS file at `dist/index.js` with a shebang, used as the `pinme` CLI binary. + +## Architecture + +- `bin/index.ts` — CLI entry point, uses `commander` to register all commands +- `bin/*.ts` — Individual command implementations (upload, save, create, bind, importCar, exportCar, delete, etc.) +- `bin/utils/` — Shared utilities: + - `config.ts` — `APP_CONFIG` singleton, all API base URLs and tuning knobs from env vars + - `apiClient.ts` — Axios client factory (`createPinmeApiClient`, `createCarApiClient`) + - `pinmeApi.ts` — High-level API wrappers (domain binding, wallet, CAR export, etc.) + - `uploadToIpfs.ts` / `uploadToIpfsSplit.ts` — IPFS upload logic (single file vs chunked) + - `webLogin.ts` — Auth token management (reads from `~/.pinme/`) + - `domainValidator.ts` — Domain name validation and DNS vs subdomain detection + - `cliError.ts` — Structured CLI error types +- `bin/services/uploadService.ts` — Upload orchestration (hash encryption, URL generation) +- `skills/` — Codex skill definitions for this project + +## Key Patterns + +- Auth: AppKey stored locally at `~/.pinme/`. Auth headers injected via `getAuthHeaders()` in `webLogin.ts`. +- API clients: Always use `createPinmeApiClient()` or `createCarApiClient()` from `apiClient.ts`, never raw axios. +- Token expiry: `pinmeApi.ts` has centralized token-expired detection (`isTokenExpired`) — all API calls should go through wrappers there. +- Config: All env-driven config lives in `APP_CONFIG` (`config.ts`). Don't read `process.env` directly elsewhere. +- The `save` command reads `pinme.toml` from project root for full-stack deploy (frontend + Cloudflare Worker + D1). + +## Code Style + +- Prettier: single quotes, trailing commas, 80 char width +- TypeScript with `strict: false`, target ESNext, module ESNext +- CLI output uses `chalk` for colors, `ora` for spinners, `inquirer` for prompts, `figlet` for banner diff --git a/CLAUDE.md b/CLAUDE.md index 1c1b435..d36bc2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,11 @@ PinMe is a zero-config CLI tool for deploying static sites to IPFS. Built with T ```bash npm run build # Production build (esbuild → dist/index.js) -npm run dev # Dev build (NODE_ENV=development) -node test/build-env.test.js # Run tests (node:test, no framework) +npm run dev # Dev build +npm run test # Unit/integration tests (Vitest) +npm run test:cli # Real CLI black-box tests against dist/index.js +npm run verify # Full PR gate: lint, typecheck, tests, build, CLI, pack +npm run test:mutation # Slow mutation tests (manual/nightly) ``` Build uses `build.js` (esbuild), NOT `rollup.config.js` (legacy, unused). esbuild reads `.env` via dotenv at build time and injects env vars as `process.env.*` defines. diff --git a/README.md b/README.md index a61166a..de15b59 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Website: [https://pinme.eth.limo/](https://pinme.eth.limo/) - [Authentication and Account Commands](#authentication-and-account-commands) - [Static Uploads and IPFS Utilities](#static-uploads-and-ipfs-utilities) - [Command Reference](#command-reference) +- [Development and Testing](#development-and-testing) - [Limits and Operational Notes](#limits-and-operational-notes) - [Examples](#examples) - [Support](#support) @@ -340,6 +341,24 @@ pinme rm | `pinme list` / `pinme ls` | Show upload history | | `pinme help` | Show CLI help | +## Development and Testing + +PinMe uses Vitest for unit/integration tests, real `dist/index.js` CLI smoke tests, npm package checks, and Stryker for slower mutation testing. + +```bash +npm run test # Unit and integration tests +npm run test:coverage # Coverage gate for core modules +npm run test:cli # Real CLI black-box tests +npm run test:pack # npm pack/package-shape checks +npm run verify # Full pull-request gate +npm run test:mutation # Slow mutation tests for manual/nightly runs +``` + +Tests must not call live PinMe/IPFS/CAR services. Use `nock`, local loopback servers, fixtures, and temporary HOME directories for API and CLI scenarios. + +For the full testing policy, layout, and mutation-testing guidance, see +[TESTING.md](TESTING.md). + ## Limits and Operational Notes - Default single-file upload limit: `100MB` diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..81443f2 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,215 @@ +# PinMe Testing Guide + +This document describes the test system for the PinMe CLI. It is intended for +maintainers, contributors, and AI coding agents working on this repository. + +## Goals + +The test suite is designed to catch regressions across the full CLI lifecycle: + +- TypeScript and lint checks. +- Unit tests for pure utility logic. +- Mocked integration tests for API wrappers and HTTP clients. +- Real CLI black-box tests against the bundled `dist/index.js`. +- npm package shape tests using `npm pack`. +- Coverage thresholds for the core in-process modules. +- Mutation testing for stricter confidence in critical logic. + +Normal tests must not call real PinMe, IPFS, CAR, GitHub template, or other +external services. Use `nock`, local fixture servers, temporary HOME +directories, and test fixtures instead. + +## Test Commands + +Use these commands from the repository root. + +```bash +npm run lint +npm run typecheck +npm run test +npm run test:coverage +npm run test:cli +npm run test:pack +npm run verify +npm run test:mutation +``` + +Command meanings: + +| Command | Purpose | +| --- | --- | +| `npm run lint` | Runs ESLint over TypeScript, MJS tests, and Vitest config files. | +| `npm run typecheck` | Runs `tsc --noEmit` with `tsconfig.test.json`. | +| `npm run test` | Runs unit, integration, source-regression, and legacy MJS tests. | +| `npm run test:coverage` | Runs the same in-process tests with V8 coverage thresholds. | +| `npm run build` | Bundles the CLI to `dist/index.js` with esbuild. | +| `npm run test:cli` | Builds and runs black-box CLI tests against `node dist/index.js`. | +| `npm run test:pack` | Builds, packs, installs, and verifies the npm package shape. | +| `npm run verify` | Main PR gate: lint, typecheck, tests, coverage, build, CLI, and pack. | +| `npm run test:mutation` | Slow strict check using Stryker mutation testing. | + +For pull requests, `npm run verify` is the required local confidence check. +Mutation testing is intentionally slower and is best run before risky releases, +large refactors, or from scheduled/manual CI jobs. + +## Test Layout + +```text +test/ + unit/ Pure utility and service tests. + integration/ Mocked API/client integration tests. + cli/ Real bundled CLI black-box tests. + pack/ npm pack and installed-tarball tests. + helpers/ Shared test helpers. + setup/ Global test setup such as nock network guards. +tests/ Existing MJS regression tests. +``` + +Important files: + +- `vitest.config.ts` controls the normal Vitest and coverage setup. +- `vitest.mutation.config.ts` narrows Stryker's test set to unit/integration + tests so mutation runs do not execute slow CLI/package black-box tests. +- `stryker.config.json` lists the core files mutation testing is allowed to + mutate. +- `.github/workflows/ci.yml` runs the open-source CI gates. + +## Coverage Policy + +Coverage focuses on in-process core modules where V8 can reliably attribute +executed code back to TypeScript source files. + +Currently covered core modules include: + +- `bin/utils/domainValidator.ts` +- `bin/utils/config.ts` +- `bin/utils/uploadLimits.ts` +- `bin/utils/apiClient.ts` +- `bin/utils/cliError.ts` +- `bin/utils/history.ts` +- `bin/utils/pinmeApi.ts` +- `bin/utils/webLogin.ts` +- `bin/services/uploadService.ts` + +The configured minimums are: + +| Metric | Minimum | +| --- | ---: | +| Statements | 85% | +| Branches | 80% | +| Functions | 85% | +| Lines | 85% | + +Command files are primarily covered by `test:cli`, which executes the bundled +CLI as a subprocess. Subprocess coverage is not reliably attributed back to the +original TypeScript command files, so those checks live in CLI tests rather than +the V8 coverage gate. + +## Mutation Policy + +Mutation testing is configured for critical utility/API/service logic rather +than the entire repository. This keeps the signal high and avoids very slow or +flaky mutants in CLI subprocess tests. + +Run: + +```bash +npm run test:mutation +``` + +Current target: + +- Overall mutation score should stay above the configured Stryker break + threshold. +- The practical project target is `80+`. +- If the score drops, first inspect survivors in: + +```text +reports/mutation/index.html +``` + +The report directory is generated output and should not be committed. + +## Network and Filesystem Rules + +Tests must be hermetic by default. + +- `test/setup/nock.ts` disables accidental external network access for normal + unit and integration tests. +- API behavior should be mocked with `nock`. +- CLI success-path tests may use local HTTP servers bound to `127.0.0.1`. +- Tests that touch user auth must isolate `HOME` and `~/.pinme` with temporary + directories. +- Do not depend on the real user's PinMe credentials. +- Do not write persistent files outside temporary directories unless the test is + explicitly verifying package/build output inside the repository. + +In restricted sandboxes, CLI success-path tests can fail with: + +```text +listen EPERM: operation not permitted 127.0.0.1 +``` + +That means the sandbox blocked local mock servers. Re-run the command in an +environment that permits local loopback listeners. + +## What To Test When Changing Code + +Use the narrowest command while developing, then run the full gate before +opening a PR. + +| Change area | Recommended tests | +| --- | --- | +| Pure utility logic | `npm run test -- test/unit/.test.ts` | +| API wrappers or Axios client behavior | `npm run test -- test/integration` | +| Auth file handling | `npm run test -- test/unit/webLogin.test.ts` | +| Upload URL/result formatting | `npm run test -- test/unit/uploadService.test.ts` | +| CLI command behavior | `npm run build && vitest run test/cli` | +| Build or package metadata | `npm run test:pack` | +| Release confidence | `npm run verify && npm run test:mutation` | + +Before finishing a non-trivial change, run: + +```bash +npm run verify +``` + +Before finishing a risky core-logic change, also run: + +```bash +npm run test:mutation +``` + +## Adding New Tests + +Choose the test layer based on what can catch the bug most directly: + +- Put pure function and local formatting tests in `test/unit/`. +- Put mocked API behavior in `test/integration/`. +- Put user-visible command behavior in `test/cli/`. +- Put publish/install behavior in `test/pack/`. +- Put old MJS regression tests in `tests/` only when matching existing + regression-test style. + +Guidelines: + +- Prefer testing public or intentionally exported helper behavior. +- Keep external services mocked. +- Use realistic fixtures for CLI tests. +- Assert both success output and failure messages when user behavior matters. +- Avoid brittle snapshots for colorful CLI output; normalize ANSI output when + necessary. +- If an internal function is hard to test, prefer a small pure helper export + over changing runtime behavior. + +## CI Expectations + +The GitHub Actions workflow keeps normal contribution feedback fast: + +- Pull requests run `npm run verify` across supported Node versions. +- Mutation testing is scheduled/manual rather than required for every PR. +- Audit checks are non-blocking so dependency advisories can be triaged without + preventing unrelated contributions. + +Generated directories such as `coverage/`, `reports/`, and `.stryker-tmp/` +should remain ignored and uncommitted. diff --git a/bin/create.ts b/bin/create.ts index 92d5e36..28218e2 100644 --- a/bin/create.ts +++ b/bin/create.ts @@ -4,7 +4,6 @@ import path from 'path'; import inquirer from 'inquirer'; import axios from 'axios'; import AdmZip from 'adm-zip'; -import { execSync } from 'child_process'; import { getAuthHeaders } from './utils/webLogin'; import { startBackgroundInstall } from './utils/installProjectDependencies'; import { @@ -18,6 +17,7 @@ import { uploadPath } from './services/uploadService'; import { printHighlightedUrl } from './utils/urlDisplay'; import { patchPrebuiltFrontendDist } from './utils/prebuiltDistConfig'; import { getValidatedWorkerMetadataContent } from './utils/workerMetadata'; +import { downloadFileWithRetries, getDownloadErrorMessage } from './utils/downloadFile'; import tracker, { getTrackErrorReason } from './utils/tracker'; import { TRACK_EVENTS, @@ -212,37 +212,30 @@ export default async function createCmd(options: CreateOptions): Promise { console.log(chalk.blue('\n2. Downloading template from repository...')); const zipPath = path.join(PROJECT_DIR, 'template.zip'); const extractDir = path.join(PROJECT_DIR, `.pinme-template-${Date.now()}`); - const templateZipUrl = getTemplateZipUrl(TEMPLATE_BRANCH); - let downloadSuccess = false; + const templateZipUrl = + process.env.PINME_TEMPLATE_ZIP_URL || getTemplateZipUrl(TEMPLATE_BRANCH); console.log(chalk.gray(` Template branch: ${TEMPLATE_BRANCH}`)); - - // Retry download up to 3 times - for (let attempt = 1; attempt <= 3 && !downloadSuccess; attempt++) { - try { - console.log(chalk.gray(` Download attempt ${attempt}/3...`)); - - // Download zip file - execSync(`curl -L --retry 3 --retry-delay 2 -o "${zipPath}" "${templateZipUrl}"`, { - stdio: 'inherit', - }); - - // Check if file was downloaded successfully - if (!fs.existsSync(zipPath) || fs.statSync(zipPath).size < 100) { - throw new Error('Downloaded file is too small or empty'); - } - - downloadSuccess = true; - } catch (downloadError: any) { - console.log(chalk.yellow(` Attempt ${attempt} failed: ${downloadError.message}`)); - if (fs.existsSync(zipPath)) { - fs.removeSync(zipPath); - } - if (attempt === 3) { - throw new Error(`Failed to download template after 3 attempts: ${downloadError.message}`); - } - } + + try { + const downloadResult = await downloadFileWithRetries(templateZipUrl, zipPath, { + attempts: 3, + retryDelayMs: 2000, + minBytes: 100, + onAttempt: (attempt, attempts) => { + console.log(chalk.gray(` Download attempt ${attempt}/${attempts}...`)); + }, + onAttemptFailure: (attempt, error) => { + console.log(chalk.yellow(` Attempt ${attempt} failed: ${getDownloadErrorMessage(error)}`)); + }, + }); + console.log(chalk.green(` Template archive downloaded (${downloadResult.bytes} bytes)`)); + } catch (error: any) { + throw createCommandError('template download', `download "${templateZipUrl}" to "${zipPath}"`, error, [ + 'Check your network connection and retry `pinme create`.', + `Verify that the template branch exists: ${TEMPLATE_BRANCH}`, + ]); } - + try { fs.ensureDirSync(extractDir); diff --git a/bin/utils/downloadFile.ts b/bin/utils/downloadFile.ts new file mode 100644 index 0000000..ea104fc --- /dev/null +++ b/bin/utils/downloadFile.ts @@ -0,0 +1,105 @@ +import axios from 'axios'; +import fs from 'fs-extra'; +import path from 'path'; +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream'; +import { promisify } from 'util'; + +const pipelineAsync = promisify(pipeline); + +export interface DownloadFileWithRetriesOptions { + attempts?: number; + retryDelayMs?: number; + minBytes?: number; + timeoutMs?: number; + request?: ( + url: string, + options: { timeoutMs: number; headers: Record }, + ) => Promise<{ data: NodeJS.ReadableStream }>; + onAttempt?: (attempt: number, attempts: number) => void; + onAttemptFailure?: (attempt: number, error: unknown) => void; +} + +export interface DownloadFileResult { + attempts: number; + bytes: number; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function getDownloadErrorMessage(error: any): string { + const status = error?.response?.status; + const statusText = error?.response?.statusText; + + if (status) { + return `HTTP ${status}${statusText ? ` ${statusText}` : ''}`; + } + + if (error?.code && error?.message) { + return `${error.code}: ${error.message}`; + } + + return error?.message || String(error); +} + +async function requestDownload( + url: string, + options: { timeoutMs: number; headers: Record }, +): Promise<{ data: NodeJS.ReadableStream }> { + return axios.get(url, { + responseType: 'stream', + timeout: options.timeoutMs, + headers: options.headers, + }); +} + +export async function downloadFileWithRetries( + url: string, + destinationPath: string, + options: DownloadFileWithRetriesOptions = {}, +): Promise { + const attempts = options.attempts ?? 3; + const retryDelayMs = options.retryDelayMs ?? 2000; + const minBytes = options.minBytes ?? 1; + const timeoutMs = options.timeoutMs ?? 120000; + const request = options.request ?? requestDownload; + const headers = { + 'User-Agent': 'pinme-cli', + }; + let lastError: unknown; + + fs.ensureDirSync(path.dirname(destinationPath)); + fs.removeSync(destinationPath); + + for (let attempt = 1; attempt <= attempts; attempt++) { + const tempPath = `${destinationPath}.download-${process.pid}-${Date.now()}-${attempt}.tmp`; + + try { + options.onAttempt?.(attempt, attempts); + + const response = await request(url, { timeoutMs, headers }); + + await pipelineAsync(response.data, createWriteStream(tempPath)); + + const bytes = fs.statSync(tempPath).size; + if (bytes < minBytes) { + throw new Error(`Downloaded file is too small (${bytes} bytes; expected at least ${minBytes} bytes).`); + } + + fs.moveSync(tempPath, destinationPath, { overwrite: true }); + return { attempts: attempt, bytes }; + } catch (error) { + lastError = error; + fs.removeSync(tempPath); + options.onAttemptFailure?.(attempt, error); + + if (attempt < attempts) { + await sleep(retryDelayMs); + } + } + } + + throw new Error(`Failed to download ${url} after ${attempts} attempts: ${getDownloadErrorMessage(lastError)}`); +} diff --git a/bin/utils/history.ts b/bin/utils/history.ts index 706d827..7bc7847 100644 --- a/bin/utils/history.ts +++ b/bin/utils/history.ts @@ -244,5 +244,6 @@ export { saveUploadHistory, getUploadHistory, displayUploadHistory, - clearUploadHistory + clearUploadHistory, + formatHistoryUrl, }; diff --git a/bin/utils/installProjectDependencies.ts b/bin/utils/installProjectDependencies.ts index 6d2341f..9fa4f46 100644 --- a/bin/utils/installProjectDependencies.ts +++ b/bin/utils/installProjectDependencies.ts @@ -204,14 +204,59 @@ function runInstall( }); } -function quoteForShell(value: string): string { +type ShellPlatform = NodeJS.Platform | 'posix'; + +interface BackgroundInstallCommand { + shellBin: string; + shellArgs: string[]; + installCommand: string; +} + +function quoteForShell(value: string, platform: ShellPlatform = process.platform): string { // tmp paths / project paths can contain spaces; wrap everything in quotes. - if (process.platform === 'win32') { + if (platform === 'win32') { return `"${value}"`; } return `'${value.replace(/'/g, `'\\''`)}'`; } +export function buildBackgroundInstallCommand( + script: InstallScript, + logPath: string, + exitCodePath: string, + platform: ShellPlatform = process.platform, +): BackgroundInstallCommand { + const args = getInstallArgs(script); + const npmCmd = platform === 'win32' ? 'npm' : getNpmCommand(); + const installCommand = `${npmCmd} ${args.map((arg) => quoteForShell(arg, platform)).join(' ')}`; + const qLog = quoteForShell(logPath, platform); + const qExit = quoteForShell(exitCodePath, platform); + + if (platform === 'win32') { + // Enable delayed expansion so !errorlevel! is evaluated after npm exits. + return { + shellBin: process.env.ComSpec || 'cmd.exe', + shellArgs: [ + '/d', + '/s', + '/v:on', + '/c', + `${installCommand} >> ${qLog} 2>&1 & echo !errorlevel! > ${qExit}`, + ], + installCommand, + }; + } + + return { + shellBin: '/bin/sh', + shellArgs: [ + '-c', + `${installCommand} >> ${qLog} 2>&1; printf '%s' "$?" > ${qExit}`, + ], + installCommand, + }; +} + /** * Start a dependency install in a detached background process and return * immediately. The child keeps running after the current CLI process exits, @@ -228,7 +273,6 @@ function quoteForShell(value: string): string { */ export function startBackgroundInstall(cwd: string): { logPath: string } { const script = getInstallScript(cwd, 'auto'); - const args = getInstallArgs(script); const logPath = path.join(cwd, INSTALL_LOG_FILE); const exitCodePath = path.join(cwd, INSTALL_EXITCODE_FILE); const pidPath = path.join(cwd, INSTALL_PID_FILE); @@ -238,24 +282,7 @@ export function startBackgroundInstall(cwd: string): { logPath: string } { fs.removeSync(pidPath); fs.writeFileSync(logPath, `[pinme] ${new Date().toISOString()} starting "npm ${script}"\n`); - const npmCmd = process.platform === 'win32' ? 'npm' : getNpmCommand(); - const installCmd = `${npmCmd} ${args.map(quoteForShell).join(' ')}`; - const qLog = quoteForShell(logPath); - const qExit = quoteForShell(exitCodePath); - - let shellBin: string; - let shellArgs: string[]; - if (process.platform === 'win32') { - // cmd.exe: chain with `&`, capture %errorlevel%. - const command = `${installCmd} >> ${qLog} 2>&1 & echo %errorlevel% > ${qExit}`; - shellBin = process.env.ComSpec || 'cmd.exe'; - shellArgs = ['/d', '/s', '/c', command]; - } else { - // POSIX sh: `$?` after the install is the install's exit code. - const command = `${installCmd} >> ${qLog} 2>&1; printf '%s' "$?" > ${qExit}`; - shellBin = '/bin/sh'; - shellArgs = ['-c', command]; - } + const { shellBin, shellArgs } = buildBackgroundInstallCommand(script, logPath, exitCodePath); const child = spawn(shellBin, shellArgs, { cwd, diff --git a/bin/utils/tracker.ts b/bin/utils/tracker.ts index 46a8671..b6fb991 100644 --- a/bin/utils/tracker.ts +++ b/bin/utils/tracker.ts @@ -82,14 +82,95 @@ function sanitizeTrackValue( } export function getTrackErrorReason(error: unknown): string { - const candidate = - (error as any)?.response?.data?.msg || - (error as any)?.response?.data?.message || - (error as any)?.message || - (error as any)?.toString?.() || - 'unknown_error'; + return sanitizeTrackValue(resolveErrorReason(error), TRACK_REASON_LIMIT) || 'unknown_error'; +} - return sanitizeTrackValue(candidate) || 'unknown_error'; +function responseDataMessage(data: any): string | undefined { + if (typeof data === 'string') { + return data; + } + + return data?.msg + || data?.message + || data?.error + || data?.data?.msg + || data?.data?.message + || data?.data?.error + || data?.errors?.[0]?.message; +} + +function normalizeReason(candidate: unknown, status?: number): string | undefined { + const value = sanitizeTrackValue(candidate, TRACK_REASON_LIMIT); + if (!value) { + return undefined; + } + + const lower = value.toLowerCase(); + + if (/^\s*]/i.test(value)) { + return 'api_returned_html'; + } + + const statusMatch = lower.match(/request failed with status code (\d{3})/); + const statusCode = status || (statusMatch ? Number(statusMatch[1]) : undefined); + if (statusCode === 520) { + return 'gateway_520'; + } + + if ( + lower.includes('token authentication failed') || + lower.includes('invalid token') || + lower.includes('token expired') || + lower.includes('authentication failed') || + lower.includes('auth failed') || + lower.includes('unauthorized') + ) { + return 'token_auth_failed'; + } + + if (lower.includes('auth not set') || lower.includes('please login first')) { + return 'auth_not_set'; + } + + if (lower.includes('login timeout')) { + return 'login_timeout'; + } + + return value; +} + +function resolveErrorReason(error: unknown, seen = new Set()): string { + if (error === undefined || error === null || seen.has(error)) { + return 'unknown_error'; + } + seen.add(error); + + const maybeError = error as any; + const responseData = maybeError?.response?.data; + const responseStatus = maybeError?.response?.status; + + const responseReason = normalizeReason( + responseDataMessage(responseData), + responseStatus, + ); + if (responseReason) { + return responseReason; + } + + if (maybeError?.cause) { + const causeReason = resolveErrorReason(maybeError.cause, seen); + if (causeReason && causeReason !== 'unknown_error') { + return causeReason; + } + } + + const messageReason = normalizeReason(maybeError?.message, responseStatus); + if (messageReason) { + return messageReason; + } + + const stringReason = normalizeReason(maybeError?.toString?.(), responseStatus); + return stringReason || 'unknown_error'; } function resolveTrackAction(event: string, data: TrackData = {}): string { diff --git a/bin/utils/webLogin.ts b/bin/utils/webLogin.ts index f128ed0..590f860 100644 --- a/bin/utils/webLogin.ts +++ b/bin/utils/webLogin.ts @@ -8,6 +8,8 @@ import os from 'os'; import path from 'path'; import { APP_CONFIG } from './config'; +/* c8 ignore start -- Browser callback/login UI is covered by manual/e2e flows; token helpers below are unit-tested. */ +/* Stryker disable all: Browser opening is OS integration; token helpers below are mutation-tested. */ // Cross-platform browser opener function openBrowser(url: string): void { const platform = process.platform; @@ -28,6 +30,7 @@ function openBrowser(url: string): void { } }); } +/* Stryker restore all */ const CONFIG_DIR = path.join(os.homedir(), '.pinme'); const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json'); @@ -54,6 +57,7 @@ const DEFAULT_OPTIONS: Required = { callbackPath: '/cli/callback', }; +/* Stryker disable all: Interactive browser login and callback HTML are covered by manual/e2e flows. */ export class WebLoginManager { private config: Required; private server: http.Server | null = null; @@ -518,6 +522,9 @@ export class WebLoginManager { // Export singleton export const webLoginManager = new WebLoginManager(); +/* Stryker restore all */ + +/* c8 ignore stop */ // Legacy interface export function setAuthToken(combined: string): AuthConfig { diff --git a/docs/superpowers/specs/2026-06-03-create-prebuilt-dist-config-replacement-design.md b/docs/superpowers/specs/2026-06-03-create-prebuilt-dist-config-replacement-design.md deleted file mode 100644 index 5dd2f4a..0000000 --- a/docs/superpowers/specs/2026-06-03-create-prebuilt-dist-config-replacement-design.md +++ /dev/null @@ -1,153 +0,0 @@ -# Create Prebuilt Dist Config Replacement Design - -## Context - -`pinme create` is optimized for first-time users by downloading a template that -already ships with `dist-worker/` and `frontend/dist/`. This avoids requiring a -fresh `npm install` or Vite build before the first deploy. - -The current create flow updates source files such as `pinme.toml`, -`frontend/.env`, `frontend/src/utils/config.ts`, and backend metadata after the -project is created. However, the first upload uses the prebuilt `frontend/dist` -directory, so values compiled into the bundle still contain empty defaults. This -breaks first-run frontend configuration such as the Worker API URL and auth -client config. - -## Goal - -Patch only the prebuilt frontend dist used by the initial `pinme create` -deployment after Pinme receives the latest project configuration from -`/create_worker`, and before the dist is uploaded to IPFS. - -Normal `pinme save`, `pinme update-web`, and developer builds remain unchanged. -After dependencies are installed, those commands rebuild from source and do not -need dist replacement. - -## Non-Goals - -- Do not add a general runtime config system. -- Do not rebuild the frontend during `pinme create`. -- Do not change `pinme save` or `pinme update-web` behavior. -- Do not use empty-string replacement as the primary mechanism. - -## Template Changes - -The template should compile stable placeholders into `frontend/dist`. - -Recommended placeholders: - -```text -__PINME_VITE_API_URL__ -__PINME_AUTH_API_KEY__ -__PINME_AUTH_DOMAIN__ -__PINME_AUTH_PROJECT_ID__ -__PINME_TENANT_ID__ -``` - -Source defaults should use those placeholders: - -- `frontend/.env.example` - - `VITE_API_URL="__PINME_VITE_API_URL__"` -- `frontend/src/utils/config.ts` - - `auth_api_key: "__PINME_AUTH_API_KEY__"` - - `auth_domain: "__PINME_AUTH_DOMAIN__"` - - `auth_project_id: "__PINME_AUTH_PROJECT_ID__"` - - `tenant_id: "__PINME_TENANT_ID__"` - -`frontend/src/pages/Auth/index.tsx` should read Firebase auth configuration from -`public_client_config` instead of only `import.meta.env.VITE_FIREBASE_*`. -Otherwise, the CLI can update `frontend/src/utils/config.ts`, but the prebuilt -auth bundle will still stay unconfigured. - -The template release process should rebuild `frontend/dist` after these source -changes. A quick release check should confirm the placeholders are present in -the built dist before publishing the template. - -## CLI Changes - -Add a create-only helper in `bin/create.ts`, or a small helper local to the -create command if it keeps the scope clearer: - -```ts -patchPrebuiltFrontendDist(frontendDistDir, workerData) -``` - -Call it in `createCmd` after the source config files are updated and before: - -```ts -uploadPath(frontendDistDir, { action: 'project_create', ... }) -``` - -Replacement mapping: - -```text -__PINME_VITE_API_URL__ <- workerData.api_domain -__PINME_AUTH_API_KEY__ <- workerData.public_client_config.auth_api_key -__PINME_AUTH_DOMAIN__ <- workerData.public_client_config.auth_domain -__PINME_AUTH_PROJECT_ID__ <- workerData.public_client_config.auth_project_id -__PINME_TENANT_ID__ <- workerData.public_client_config.tenant_id -``` - -If `public_client_config` is absent, replace auth placeholders with empty -strings so the Auth Demo clearly remains disabled. `workerData.api_domain` is -required; if it is missing, fail create before upload with a clear config error. - -## File Scanning - -Patch text files under `frontend/dist`, including: - -- `.html` -- `.js` -- `.css` -- `.json` -- `.map` - -Binary files and unrelated assets should be skipped. - -The helper should count replacements and print a concise success line such as: - -```text -Patched prebuilt frontend dist config -``` - -## Validation - -Before upload, validate: - -- `__PINME_VITE_API_URL__` no longer exists in `frontend/dist`. -- If `public_client_config` was returned, none of the auth placeholders remain. -- If no matching API URL placeholder was found, fail create with a message that - the template prebuilt dist is missing required Pinme config placeholders. - -This prevents uploading a known-bad first-run frontend. - -## Error Handling - -Use the existing CLI error style with `createConfigError` for local template -problems. The error should explain: - -- the prebuilt dist is missing required placeholders; -- the template should be rebuilt from the placeholder-enabled source; -- users can run `npm install`, `npm run build:frontend`, and `pinme save` as a - recovery path after create, if needed. - -## Tests - -Add focused unit coverage for the replacement helper where practical: - -- API URL placeholder is replaced. -- Auth placeholders are replaced when `public_client_config` exists. -- Auth placeholders are replaced with empty strings when auth config is absent. -- Missing API URL placeholder fails validation. -- Binary or unsupported files are skipped. - -Add a template-side build check or script-level assertion that the published -`frontend/dist` includes `__PINME_VITE_API_URL__`. - -## Rollout - -1. Update template source defaults and Auth config source. -2. Rebuild template `frontend/dist`. -3. Update CLI `pinme create` to patch the prebuilt dist before first upload. -4. Verify `pinme create ` uploads a frontend whose dist contains the real - Worker API URL and auth config values. diff --git a/package-lock.json b/package-lock.json index 23f8b12..dcf16bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pinme", - "version": "2.0.6", + "version": "2.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pinme", - "version": "2.0.6", + "version": "2.0.10", "license": "MIT", "dependencies": { "adm-zip": "^0.5.17", @@ -34,7 +34,12 @@ "@rollup/plugin-commonjs": "22.0.2", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "14.1.0", + "@stryker-mutator/core": "^7.3.0", + "@stryker-mutator/vitest-runner": "^7.3.0", "@types/adm-zip": "^0.5.8", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitest/coverage-v8": "^0.34.6", "dotenv": "16.5.0", "esbuild": "0.25.2", "eslint": "8.33.0", @@ -42,23 +47,43 @@ "eslint-config-prettier": "8.6.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-prettier": "4.2.1", + "execa": "^7.2.0", + "fast-check": "^3.23.2", + "nock": "^13.5.6", "prettier": "2.8.3", "rollup": "2.79.2", "rollup-plugin-copy": "3.5.0", - "rollup-plugin-terser": "7.0.2" + "rollup-plugin-terser": "7.0.2", + "tmp-promise": "^3.0.3", + "typescript": "^5.4.5", + "vitest": "^0.34.6" }, "engines": { "node": ">= 16.13.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -66,16 +91,546 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.9.tgz", + "integrity": "sha512-hJhBCb0+NnTWybvWq2WpbCYDOcflSbx0t+BYP65e5R9GVnukiDTi+on5bFkk4p7QGuv190H6KfNiV9Knf/3cZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.23.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-decorators": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", + "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-typescript": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -501,6 +1056,35 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -1264,6 +1848,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1360,6 +1954,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1410,6 +2027,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -1551,6 +2181,939 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/api": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-7.3.0.tgz", + "integrity": "sha512-0tiQF0E38ypgg2fb2a4wbr2wpu4ugY7HwwsgrI9NttY1EojOS0BtaKHo1DIrj5SVMRXq0kaMgl5h2ohSuysvRA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "2.0.3", + "mutation-testing-report-schema": "2.0.3", + "tslib": "~2.6.0", + "typed-inject": "~4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@stryker-mutator/api/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stryker-mutator/core": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-7.3.0.tgz", + "integrity": "sha512-O9m2jEnJXbKBlj27/ps9nGCpm0HtQC0YlNV/aenocmERnySnvqEM6bwxvQ4apK5bad8ZyGJyhDIyJrwoVGmfVQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "7.3.0", + "@stryker-mutator/instrumenter": "7.3.0", + "@stryker-mutator/util": "7.3.0", + "ajv": "~8.12.0", + "chalk": "~5.3.0", + "commander": "~11.1.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.2.1", + "execa": "~8.0.0", + "file-url": "~4.0.0", + "get-port": "~7.0.0", + "glob": "~10.3.0", + "inquirer": "~9.2.0", + "lodash.groupby": "~4.6.0", + "log4js": "~6.9.0", + "minimatch": "~9.0.1", + "mutation-testing-elements": "2.0.3", + "mutation-testing-metrics": "2.0.3", + "mutation-testing-report-schema": "2.0.3", + "npm-run-path": "~5.1.0", + "progress": "~2.0.0", + "rxjs": "~7.8.0", + "semver": "^7.3.5", + "source-map": "~0.7.3", + "tree-kill": "~1.2.2", + "tslib": "2.6.2", + "typed-inject": "~4.0.0", + "typed-rest-client": "~1.8.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@stryker-mutator/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/emoji-regex": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", + "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stryker-mutator/core/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/inquirer": { + "version": "9.2.23", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.23.tgz", + "integrity": "sha512-kod5s+FBPIDM2xiy9fu+6wdU/SkK5le5GS9lh4FEBjBHqiMgD9lLFbCbuqFNAjNL2ZOy9Wd9F694IOzN9pZHBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.3", + "@ljharb/through": "^2.3.13", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@stryker-mutator/core/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@stryker-mutator/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stryker-mutator/core/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-7.3.0.tgz", + "integrity": "sha512-RdfQF08GclNdKldG3rH9YztapPhfTYsc90p8Tev+b6yZJSpk1j8mKZRMjxk/mylDtXFZZ2IVhI9txAt2YYT+OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.23.0", + "@babel/generator": "~7.23.0", + "@babel/parser": "~7.23.0", + "@babel/plugin-proposal-decorators": "~7.23.0", + "@babel/preset-typescript": "~7.23.0", + "@stryker-mutator/api": "7.3.0", + "@stryker-mutator/util": "7.3.0", + "angular-html-parser": "~4.0.0", + "weapon-regex": "~1.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-7.3.0.tgz", + "integrity": "sha512-bdFvuw7F3LC05dOFqgGjuipLt8ng5uXyjjdKeqqeTowm1wAyeDt0GTQKBuiINSAtcZxN75wTXq4DsCZXb/LMjw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stryker-mutator/vitest-runner": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-7.3.0.tgz", + "integrity": "sha512-CrG9n0G+iseFOKdHdNhrDhMR8FfVU+72pbSaVAkug7MlVamvnqmjTLZwbus8Mv4VkPid6KU6MmP8kniUfcIhVQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "7.3.0", + "@stryker-mutator/util": "7.3.0", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "~7.3.0", + "vitest": ">=0.31.2" + } + }, + "node_modules/@stryker-mutator/vitest-runner/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, "node_modules/@types/adm-zip": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz", @@ -1561,6 +3124,23 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-subset": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/chai": "<5.2.0" + } + }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -1589,6 +3169,20 @@ "@types/node": "*" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1623,6 +3217,445 @@ "@types/node": "*" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", + "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.32.0 <1" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1658,6 +3691,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adm-zip": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", @@ -1690,6 +3736,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/angular-html-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-4.0.1.tgz", + "integrity": "sha512-x9SLf2jNNh3nG+haVIwKX/GVW8PcvSRmkeT9WqTDYSAVuwT9IzwEyVm09FCZpOo/dtFRxE9vaNXqcAf/MIxphg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1936,6 +3995,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2118,6 +4187,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -2218,6 +4300,40 @@ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "license": "MIT" }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -2271,6 +4387,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2331,6 +4457,46 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "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/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2351,6 +4517,19 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "license": "MIT" }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -2466,6 +4645,13 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -2473,6 +4659,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2578,6 +4771,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", @@ -2602,6 +4805,19 @@ } } }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2676,6 +4892,23 @@ "node": ">=0.4.0" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2736,6 +4969,13 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true, + "license": "ISC" + }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -2953,6 +5193,16 @@ "@esbuild/win32-x64": "0.25.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3472,6 +5722,72 @@ "bare-events": "^2.7.0" } }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -3486,6 +5802,29 @@ "node": ">=4" } }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3600,6 +5939,19 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-url": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-4.0.0.tgz", + "integrity": "sha512-vRCdScQ6j3Ku6Kd7W1kZk9c++5SqD6Xz5Jotrjr/nkY714M14RFHy/AAVA2WQvpsqVAVgTbDrYyBpU205F0cLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3826,6 +6178,26 @@ "node": ">= 0.4" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3851,6 +6223,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz", + "integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3865,6 +6250,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -3997,6 +6395,13 @@ "dev": true, "license": "MIT" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -4121,6 +6526,23 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4858,6 +7280,83 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -4948,6 +7447,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4969,6 +7481,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -5066,6 +7585,19 @@ "node": ">= 0.8.0" } }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5088,6 +7620,13 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5107,6 +7646,33 @@ "node": ">=4" } }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5123,6 +7689,35 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5238,6 +7833,26 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5245,12 +7860,55 @@ "dev": true, "license": "MIT" }, + "node_modules/mutation-testing-elements": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-2.0.3.tgz", + "integrity": "sha512-V00F5dVriVZTPoDcflX2Lp+/cA1LrkX9RwPntrrAEmM8OLEUG+jSZIJeYImTGK/opW5yD+q9ugykVjHbw2KQTg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-2.0.3.tgz", + "integrity": "sha512-pvrrE8Qf5xuimkm+TYUwX3g6Op6K4jE2/tD4NX8UZdTzuT/NHwAJw/YUXI7UJSA9M9Jpz9+VCjB31YnAX6wm7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "2.0.3" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-2.0.3.tgz", + "integrity": "sha512-+x6ssyq4xVkUyHbbbbiU1pCla7QHO/VRaxfHsOb4JGCw+56EtCJ4w4wQuQ24J5DYTRCAZ5y2oBk7DwP8UXWbwg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "license": "ISC" }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5258,6 +7916,31 @@ "dev": true, "license": "MIT" }, + "node_modules/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5267,6 +7950,35 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5613,6 +8325,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5633,6 +8362,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "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", @@ -5643,6 +8391,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5682,6 +8459,34 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -5697,6 +8502,26 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5713,6 +8538,39 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5744,6 +8602,13 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -5847,6 +8712,16 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -5902,6 +8777,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -6299,6 +9181,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6325,6 +9214,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -6344,6 +9243,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6358,6 +9271,56 @@ "node": ">= 0.4" } }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/streamx": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", @@ -6501,6 +9464,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "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", @@ -6514,6 +9490,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6586,6 +9575,21 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -6608,6 +9612,33 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -6620,6 +9651,26 @@ "node": ">=0.6.0" } }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmp-promise/node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6633,6 +9684,29 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -6652,6 +9726,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6665,6 +9749,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6756,6 +9850,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-inject": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-4.0.0.tgz", + "integrity": "sha512-OuBL3G8CJlS/kjbGV/cN8Ni32+ktyyi6ADDZpKvksbX0fYBV5WcukhRCYa7WqLce7dY/Br2dwtmJ9diiadLFpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6775,6 +9912,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -6791,6 +9935,37 @@ "node": ">= 10.0.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6820,6 +9995,675 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -6829,6 +10673,13 @@ "defaults": "^1.0.3" } }, + "node_modules/weapon-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.1.1.tgz", + "integrity": "sha512-b0RmqduiSUKyKFamrpU+UK78Jm65/6CgKq1zoWFaS9PM7vwNK4RWrjmX1jREs3pLmG7botsgMLVOltxDR7RGRw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6933,6 +10784,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7069,6 +10937,13 @@ } } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 962f9f0..cf4d358 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,15 @@ "main": "dist/index.js", "scripts": { "build": "node build.js", - "dev": "NODE_ENV=development node build.js", + "dev": "node build.js", + "lint": "eslint --ext .ts,.mjs bin test tests vitest.config.ts vitest.mutation.config.ts", + "typecheck": "tsc --noEmit --project tsconfig.test.json", + "test": "vitest run test/unit test/integration test/login-tracking-source.test.mjs tests", + "test:coverage": "vitest run --coverage test/unit test/integration test/login-tracking-source.test.mjs tests", + "test:cli": "npm run build && vitest run test/cli", + "test:pack": "npm run build && vitest run test/pack", + "test:mutation": "stryker run", + "verify": "npm run lint && npm run typecheck && npm run test && npm run test:coverage && npm run build && npm run test:cli && npm run test:pack", "prepublishOnly": "npm run build" }, "bin": { @@ -17,7 +25,6 @@ "files": [ "dist" ], - "keywords": [ "ipfs", "cli", @@ -49,7 +56,12 @@ "@rollup/plugin-commonjs": "22.0.2", "@rollup/plugin-json": "4.1.0", "@rollup/plugin-node-resolve": "14.1.0", + "@stryker-mutator/core": "^7.3.0", + "@stryker-mutator/vitest-runner": "^7.3.0", "@types/adm-zip": "^0.5.8", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitest/coverage-v8": "^0.34.6", "dotenv": "16.5.0", "esbuild": "0.25.2", "eslint": "8.33.0", @@ -57,10 +69,16 @@ "eslint-config-prettier": "8.6.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-prettier": "4.2.1", + "execa": "^7.2.0", + "fast-check": "^3.23.2", + "nock": "^13.5.6", "prettier": "2.8.3", "rollup": "2.79.2", "rollup-plugin-copy": "3.5.0", - "rollup-plugin-terser": "7.0.2" + "rollup-plugin-terser": "7.0.2", + "tmp-promise": "^3.0.3", + "typescript": "^5.4.5", + "vitest": "^0.34.6" }, "engines": { "node": ">= 16.13.0" diff --git a/stryker.config.json b/stryker.config.json new file mode 100644 index 0000000..774a5c9 --- /dev/null +++ b/stryker.config.json @@ -0,0 +1,28 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "testRunner": "vitest", + "coverageAnalysis": "perTest", + "mutate": [ + "bin/utils/domainValidator.ts", + "bin/utils/config.ts", + "bin/utils/uploadLimits.ts", + "bin/utils/apiClient.ts", + "bin/utils/cliError.ts", + "bin/utils/history.ts", + "bin/utils/pinmeApi.ts", + "bin/utils/webLogin.ts", + "bin/services/uploadService.ts" + ], + "vitest": { + "configFile": "vitest.mutation.config.ts" + }, + "thresholds": { + "high": 80, + "low": 60, + "break": 76 + }, + "reporters": ["progress", "clear-text", "html"], + "htmlReporter": { + "fileName": "reports/mutation/index.html" + } +} diff --git a/test/cli/basic.test.ts b/test/cli/basic.test.ts new file mode 100644 index 0000000..52c8d05 --- /dev/null +++ b/test/cli/basic.test.ts @@ -0,0 +1,121 @@ +import path from 'path'; +import { describe, expect, test } from 'vitest'; +import { + createTempHome, + repoRoot, + runCli, + writeAuthConfig, +} from '../helpers/cliRunner'; + +describe('pinme CLI', () => { + test('prints help for --help', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['--help'], { home: temp.home }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Usage: pinme'); + expect(result.stdout).toContain('upload'); + } finally { + await temp.cleanup(); + } + }); + + test('prints package version for --version', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['--version'], { home: temp.home }); + + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/); + } finally { + await temp.cleanup(); + } + }); + + test('shows banner and help with no arguments', async () => { + const temp = await createTempHome(); + try { + const result = await runCli([], { home: temp.home }); + const output = `${result.stdout}\n${result.stderr}`; + + expect(result.exitCode).toBe(1); + expect(output).toContain('Usage: pinme'); + expect(output).toContain('Examples:'); + } finally { + await temp.cleanup(); + } + }); + + test('list reports empty upload history in isolated HOME', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['list'], { home: temp.home }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('No upload history found.'); + } finally { + await temp.cleanup(); + } + }); + + test('upload exits before network work when auth is missing', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['upload', 'test/fixtures/site'], { + home: temp.home, + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Please login first. Run: pinme login'); + } finally { + await temp.cleanup(); + } + }); + + test('bind exits before network work when auth is missing', async () => { + const temp = await createTempHome(); + try { + const result = await runCli( + ['bind', 'test/fixtures/site', '--domain', 'demo'], + { home: temp.home }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Please login first'); + } finally { + await temp.cleanup(); + } + }); + + test('bind rejects malformed DNS domains before API calls', async () => { + const temp = await createTempHome(); + try { + await writeAuthConfig(temp.home); + const result = await runCli( + ['bind', 'test/fixtures/site', '--domain', '-bad.com', '--dns'], + { home: temp.home }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Labels cannot start or end with hyphens'); + } finally { + await temp.cleanup(); + } + }); + + test('save exits before project work when auth is missing', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['save'], { + home: temp.home, + cwd: path.join(repoRoot, 'test', 'fixtures', 'site'), + }); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Auth not set. Run: pinme login'); + } finally { + await temp.cleanup(); + } + }); +}); diff --git a/test/cli/commands.test.ts b/test/cli/commands.test.ts new file mode 100644 index 0000000..1231dda --- /dev/null +++ b/test/cli/commands.test.ts @@ -0,0 +1,163 @@ +import path from 'path'; +import { writeFile } from 'fs/promises'; +import { describe, expect, test } from 'vitest'; +import { + createTempHome, + repoRoot, + runCli, + writeAuthConfig, +} from '../helpers/cliRunner'; + +function outputOf(result: { stdout: string; stderr: string }): string { + return `${result.stdout}\n${result.stderr}`; +} + +describe('pinme command-level guards', () => { + test('create requires a local login before project creation', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['create', 'demo-project'], { + home: temp.home, + }); + + expect(result.exitCode).toBe(1); + expect(outputOf(result)).toContain('Auth not set. Run: pinme login'); + } finally { + await temp.cleanup(); + } + }); + + test('import requires a local login before reading CAR input', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['import', 'test/fixtures/site'], { + home: temp.home, + }); + + expect(result.exitCode).toBe(0); + expect(outputOf(result)).toContain('Please login first. Run: pinme login'); + } finally { + await temp.cleanup(); + } + }); + + test('import rejects nonexistent paths before upload', async () => { + const temp = await createTempHome(); + try { + await writeAuthConfig(temp.home); + const result = await runCli(['import', 'does-not-exist.car'], { + home: temp.home, + }); + + expect(result.exitCode).toBe(0); + expect(outputOf(result)).toContain('path does-not-exist.car does not exist'); + } finally { + await temp.cleanup(); + } + }); + + test('export rejects invalid CID arguments before CAR API calls', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['export', 'not-a-cid', '--output', temp.home], { + home: temp.home, + }); + + expect(result.exitCode).toBe(0); + expect(outputOf(result)).toContain('Invalid CID format'); + } finally { + await temp.cleanup(); + } + }); + + test('export rejects output paths that are files before CAR API calls', async () => { + const temp = await createTempHome(); + try { + const filePath = path.join(temp.home, 'not-a-directory'); + await writeFile(filePath, 'file'); + const result = await runCli( + ['export', 'bafyvalidcid', '--output', filePath], + { home: temp.home }, + ); + + expect(result.exitCode).toBe(0); + expect(outputOf(result)).toContain('exists but is not a directory'); + } finally { + await temp.cleanup(); + } + }); + + test('delete requires a local login before resolving project deletion', async () => { + const temp = await createTempHome(); + try { + const result = await runCli(['delete', 'demo-project', '--force'], { + home: temp.home, + }); + + expect(result.exitCode).toBe(1); + expect(outputOf(result)).toContain('Auth not set. Run: pinme login'); + } finally { + await temp.cleanup(); + } + }); + + test('delete requires a project name when no pinme.toml is present', async () => { + const temp = await createTempHome(); + try { + await writeAuthConfig(temp.home); + const result = await runCli(['delete', '--force'], { + home: temp.home, + cwd: path.join(repoRoot, 'test', 'fixtures', 'site'), + }); + + expect(result.exitCode).toBe(1); + expect(outputOf(result)).toContain('Cannot find project name'); + } finally { + await temp.cleanup(); + } + }); + + test.each([ + ['save', ['save'], 'Auth not set. Run: pinme login'], + ['update-web', ['update-web'], 'Auth not set. Run: pinme login'], + ['update-worker', ['update-worker'], 'Auth not set. Run: pinme login'], + ['update-db', ['update-db'], 'Auth not set. Run: pinme login'], + ])('%s requires login before project work', async (_name, args, message) => { + const temp = await createTempHome(); + try { + const result = await runCli(args, { + home: temp.home, + cwd: path.join(repoRoot, 'test', 'fixtures', 'site'), + }); + + expect(result.exitCode).toBe(1); + expect(outputOf(result)).toContain(message); + } finally { + await temp.cleanup(); + } + }); + + test.each([ + ['save', ['save'], 'pinme.toml` not found'], + ['update-web', ['update-web'], 'pinme.toml` not found'], + ['update-worker', ['update-worker'], 'pinme.toml` not found'], + ['update-db', ['update-db'], 'pinme.toml` not found'], + ])( + '%s validates project config before build or deploy work', + async (_name, args, message) => { + const temp = await createTempHome(); + try { + await writeAuthConfig(temp.home); + const result = await runCli(args, { + home: temp.home, + cwd: path.join(repoRoot, 'test', 'fixtures', 'site'), + }); + + expect(result.exitCode).toBe(1); + expect(outputOf(result)).toContain(message); + } finally { + await temp.cleanup(); + } + }, + ); +}); diff --git a/test/cli/success.test.ts b/test/cli/success.test.ts new file mode 100644 index 0000000..9a24eff --- /dev/null +++ b/test/cli/success.test.ts @@ -0,0 +1,959 @@ +import path from 'path'; +import { chmod, mkdir, readFile, writeFile } from 'fs/promises'; +import AdmZip from 'adm-zip'; +import { describe, expect, test } from 'vitest'; +import { + buildCliWithEnv, + createTempHome, + repoRoot, + runCli, + writeAuthConfig, +} from '../helpers/cliRunner'; +import { startLocalHttpServer } from '../helpers/localHttpServer'; + +function outputOf(result: { stdout: string; stderr: string }): string { + return `${result.stdout}\n${result.stderr}`; +} + +function createTemplateZipBuffer(): Buffer { + const zip = new AdmZip(); + const root = 'pinme-worker-template-main'; + + zip.addFile( + `${root}/package.json`, + Buffer.from('{"scripts":{"build":"echo build"}}\n'), + ); + zip.addFile( + `${root}/pinme.toml`, + Buffer.from('project_name = "template-project"\n'), + ); + zip.addFile( + `${root}/backend/wrangler.toml`, + Buffer.from('name = "template-project"\n'), + ); + zip.addFile( + `${root}/dist-worker/worker.js`, + Buffer.from('export default { fetch() { return new Response("ok"); } };\n'), + ); + zip.addFile( + `${root}/db/001_init.sql`, + Buffer.from('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY);\n'), + ); + zip.addFile( + `${root}/frontend/.env.example`, + Buffer.from('VITE_API_URL=https://your-project.example\n'), + ); + zip.addFile( + `${root}/frontend/src/utils/config.ts`, + Buffer.from('export const existing = true;\n'), + ); + zip.addFile( + `${root}/frontend/dist/index.html`, + Buffer.from( + '\n', + ), + ); + + return zip.toBuffer(); +} + +async function createFakeNpmBin(root: string): Promise { + const binDir = path.join(root, 'fake-bin'); + const npmPath = path.join(binDir, 'npm'); + await mkdir(binDir, { recursive: true }); + await writeFile(npmPath, '#!/bin/sh\nexit 0\n'); + await chmod(npmPath, 0o755); + return binDir; +} + +describe('pinme CLI success paths with local APIs', () => { + test('upload posts chunk workflow and prints public and management URLs', async () => { + const temp = await createTempHome(); + let bundle: Awaited> | undefined; + const server = await startLocalHttpServer((request, response) => { + const bodyText = request.body.toString('utf8'); + + if (request.method === 'POST' && request.url === '/chunk/init') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(JSON.parse(bodyText)).toMatchObject({ + file_name: 'index.html', + is_directory: false, + uid: '0x1234567890abcdef', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + session_id: 'session-1', + total_chunks: 1, + chunk_size: 1024 * 1024, + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/upload') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(bodyText).toContain('session-1'); + expect(bodyText).toContain('0x1234567890abcdef'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { chunk_index: 0, chunk_size: request.body.length }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/complete') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(JSON.parse(bodyText)).toMatchObject({ + session_id: 'session-1', + uid: '0x1234567890abcdef', + action: 'upload', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { trace_id: 'trace-1' }, + }), + ); + return; + } + + if ( + request.method === 'GET' && + request.url === + '/up_status?trace_id=trace-1&uid=0x1234567890abcdef' + ) { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + is_ready: true, + upload_rst: { + Bytes: 32, + Name: 'index.html', + Size: 32, + Hash: 'bafy-success', + ShortUrl: 'short-success', + }, + }, + }), + ); + return; + } + + if (request.method === 'GET' && request.url === '/root_domain') { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { domain: 'pinme.test' }, + }), + ); + return; + } + + response.writeHead(404, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 404, msg: 'unexpected request' })); + }); + + try { + bundle = await buildCliWithEnv({ + IPFS_API_URL: server.baseUrl, + PINME_API_BASE: server.baseUrl, + POLL_INTERVAL_SECONDS: '0', + MAX_POLL_TIME_MINUTES: '1', + }); + await writeAuthConfig(temp.home); + const result = await runCli( + ['upload', path.join(repoRoot, 'test', 'fixtures', 'site', 'index.html')], + { + home: temp.home, + cliPath: bundle.cliPath, + timeout: 20000, + }, + ); + + expect(result.exitCode, outputOf(result)).toBe(0); + expect(outputOf(result)).toContain('URL'); + expect(outputOf(result)).toContain('https://short-success.pinme.test'); + expect(outputOf(result)).toContain('Management URL'); + expect(outputOf(result)).toContain( + 'https://preview.pinme.test/#/preview/bafy-success', + ); + expect(server.requests.map((request) => request.url)).toEqual([ + '/chunk/init', + '/chunk/upload', + '/chunk/complete', + '/up_status?trace_id=trace-1&uid=0x1234567890abcdef', + '/root_domain', + ]); + } finally { + if (bundle) { + await bundle.cleanup(); + } + await server.close(); + await temp.cleanup(); + } + }); + + test('bind uploads content and binds a PinMe subdomain', async () => { + const temp = await createTempHome(); + let bundle: Awaited> | undefined; + const server = await startLocalHttpServer((request, response) => { + const bodyText = request.body.toString('utf8'); + + if (request.method === 'GET' && request.url === '/pay/wallet/balance') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + msg: 'ok', + data: { wallet_balance_usd: 10 }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/check_domain') { + expect(JSON.parse(bodyText)).toEqual({ domain_name: 'demo-bind' }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ data: { is_valid: true } })); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/init') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(JSON.parse(bodyText)).toMatchObject({ + file_name: 'index.html', + is_directory: false, + uid: '0x1234567890abcdef', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + session_id: 'bind-session-1', + total_chunks: 1, + chunk_size: 1024 * 1024, + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/upload') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(bodyText).toContain('bind-session-1'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { chunk_index: 0, chunk_size: request.body.length }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/complete') { + expect(JSON.parse(bodyText)).toMatchObject({ + session_id: 'bind-session-1', + uid: '0x1234567890abcdef', + action: 'bind', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { trace_id: 'bind-trace-1' }, + }), + ); + return; + } + + if ( + request.method === 'GET' && + request.url === + '/up_status?trace_id=bind-trace-1&uid=0x1234567890abcdef' + ) { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + is_ready: true, + upload_rst: { + Bytes: 32, + Name: 'index.html', + Size: 32, + Hash: 'bafy-bind-success', + }, + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/bind_pinme_domain') { + expect(JSON.parse(bodyText)).toEqual({ + domain_name: 'demo-bind', + hash: 'bafy-bind-success', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 200, msg: 'ok' })); + return; + } + + if (request.method === 'GET' && request.url === '/root_domain') { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { domain: 'pinme.test' }, + }), + ); + return; + } + + response.writeHead(404, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 404, msg: 'unexpected request' })); + }); + + try { + bundle = await buildCliWithEnv({ + IPFS_API_URL: server.baseUrl, + PINME_API_BASE: server.baseUrl, + POLL_INTERVAL_SECONDS: '0', + MAX_POLL_TIME_MINUTES: '1', + }); + await writeAuthConfig(temp.home); + const result = await runCli( + [ + 'bind', + path.join(repoRoot, 'test', 'fixtures', 'site', 'index.html'), + '--domain', + 'demo-bind', + ], + { + home: temp.home, + cliPath: bundle.cliPath, + timeout: 20000, + }, + ); + + expect(result.exitCode, outputOf(result)).toBe(0); + const output = outputOf(result); + expect(output).toContain('Wallet balance available: $10.00'); + expect(output).toContain('Domain available: demo-bind'); + expect(output).toContain('Upload success, CID: bafy-bind-success'); + expect(output).toContain('Bind success: demo-bind'); + expect(output).toContain('Visit: https://demo-bind.pinme.test'); + expect(server.requests.map((request) => request.url)).toEqual([ + '/pay/wallet/balance', + '/check_domain', + '/chunk/init', + '/chunk/upload', + '/chunk/complete', + '/up_status?trace_id=bind-trace-1&uid=0x1234567890abcdef', + '/bind_pinme_domain', + '/root_domain', + ]); + } finally { + if (bundle) { + await bundle.cleanup(); + } + await server.close(); + await temp.cleanup(); + } + }); + + test('delete --force posts project deletion and prints success', async () => { + const temp = await createTempHome(); + const server = await startLocalHttpServer((request, response) => { + expect(request.method).toBe('POST'); + expect(request.url).toBe('/delete_project'); + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(JSON.parse(request.body.toString('utf8'))).toEqual({ + project_name: 'demo-project', + }); + + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + project_name: 'demo-project', + domain_deleted: true, + worker_deleted: true, + database_deleted: true, + }, + }), + ); + }); + + try { + const bundle = await buildCliWithEnv({ + PINME_API_BASE: server.baseUrl, + }); + await writeAuthConfig(temp.home); + const result = await runCli( + ['delete', 'demo-project', '--force'], + { + home: temp.home, + cliPath: bundle.cliPath, + }, + ); + + expect(result.exitCode, outputOf(result)).toBe(0); + expect(outputOf(result)).toContain('Project deleted successfully'); + expect(server.requests).toHaveLength(1); + await bundle.cleanup(); + } finally { + await server.close(); + await temp.cleanup(); + } + }); + + test('update-db uploads SQL files and prints completion', async () => { + const temp = await createTempHome(); + const server = await startLocalHttpServer((request, response) => { + expect(request.method).toBe('POST'); + expect(request.url).toBe('/update_db?project_name=fixture-project'); + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(request.body.toString('utf8')).toContain( + 'CREATE TABLE IF NOT EXISTS notes', + ); + + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + results: [ + { + filename: '001_init.sql', + status: 'complete', + num_queries: 1, + duration: 3, + changes: 0, + rows_read: 0, + rows_written: 0, + }, + ], + }, + }), + ); + }); + + try { + const bundle = await buildCliWithEnv({ + PINME_API_BASE: server.baseUrl, + }); + await writeAuthConfig(temp.home); + const result = await runCli(['update-db'], { + home: temp.home, + cwd: path.join(repoRoot, 'test', 'fixtures', 'project'), + cliPath: bundle.cliPath, + }); + + expect(result.exitCode, outputOf(result)).toBe(0); + expect(outputOf(result)).toContain('Database update complete.'); + expect(server.requests).toHaveLength(1); + await bundle.cleanup(); + } finally { + await server.close(); + await temp.cleanup(); + } + }); + + test('import uploads CAR content and binds a PinMe subdomain', async () => { + const temp = await createTempHome(); + let bundle: Awaited> | undefined; + const server = await startLocalHttpServer((request, response) => { + const bodyText = request.body.toString('utf8'); + + if (request.method === 'POST' && request.url === '/check_domain') { + expect(JSON.parse(bodyText)).toEqual({ domain_name: 'demo-import' }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ data: { is_valid: true } })); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/init') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(JSON.parse(bodyText)).toMatchObject({ + file_name: 'index.html', + is_directory: false, + uid: '0x1234567890abcdef', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + session_id: 'import-session-1', + total_chunks: 1, + chunk_size: 1024 * 1024, + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/upload') { + expect(bodyText).toContain('import-session-1'); + expect(bodyText).toContain('0x1234567890abcdef'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { chunk_index: 0, chunk_size: request.body.length }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/complete') { + expect(JSON.parse(bodyText)).toMatchObject({ + session_id: 'import-session-1', + uid: '0x1234567890abcdef', + action: 'import', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { trace_id: 'import-trace-1' }, + }), + ); + return; + } + + if ( + request.method === 'GET' && + request.url === + '/up_status?trace_id=import-trace-1&uid=0x1234567890abcdef' + ) { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + is_ready: true, + upload_rst: { + Bytes: 32, + Name: 'index.html', + Size: 32, + Hash: 'bafy-import-success', + }, + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/bind_pinme_domain') { + expect(JSON.parse(bodyText)).toEqual({ + domain_name: 'demo-import', + hash: 'bafy-import-success', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 200, msg: 'ok' })); + return; + } + + if (request.method === 'GET' && request.url === '/root_domain') { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { domain: 'pinme.test' }, + }), + ); + return; + } + + response.writeHead(404, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 404, msg: 'unexpected request' })); + }); + + try { + bundle = await buildCliWithEnv({ + IPFS_API_URL: server.baseUrl, + PINME_API_BASE: server.baseUrl, + POLL_INTERVAL_SECONDS: '0', + MAX_POLL_TIME_MINUTES: '1', + }); + await writeAuthConfig(temp.home); + const result = await runCli( + [ + 'import', + path.join(repoRoot, 'test', 'fixtures', 'site', 'index.html'), + '--domain', + 'demo-import', + ], + { + home: temp.home, + cliPath: bundle.cliPath, + timeout: 20000, + }, + ); + + expect(result.exitCode, outputOf(result)).toBe(0); + const output = outputOf(result); + expect(output).toContain('Domain available: demo-import'); + expect(output).toContain('URL'); + expect(output).toContain('bafy-import-success'); + expect(output).toContain('Bind success: demo-import'); + expect(output).toContain( + 'Visit (Pinme subdomain example): https://demo-import.pinme.test', + ); + expect(server.requests.map((request) => request.url)).toEqual([ + '/check_domain', + '/chunk/init', + '/chunk/upload', + '/chunk/complete', + '/up_status?trace_id=import-trace-1&uid=0x1234567890abcdef', + '/bind_pinme_domain', + '/root_domain', + ]); + } finally { + if (bundle) { + await bundle.cleanup(); + } + await server.close(); + await temp.cleanup(); + } + }); + + test('export requests CAR generation and downloads the completed file', async () => { + const temp = await createTempHome(); + let bundle: Awaited> | undefined; + const cid = 'bafyexportsuccess'; + const outputDir = path.join(temp.home, 'exports'); + const server = await startLocalHttpServer((request, response) => { + if ( + request.method === 'POST' && + request.url === + `/car/export?cid=${cid}&uid=0x1234567890abcdef` + ) { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + msg: 'ok', + data: { + cid, + status: 'processing', + task_id: 'export-task-1', + }, + }), + ); + return; + } + + if ( + request.method === 'GET' && + request.url === '/car/export/status?task_id=export-task-1' + ) { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + msg: 'ok', + data: { + task_id: 'export-task-1', + cid, + status: 'completed', + download_url: `${server.baseUrl}/downloads/${cid}.car`, + }, + }), + ); + return; + } + + if (request.method === 'GET' && request.url === `/downloads/${cid}.car`) { + const body = Buffer.from('fixture car payload'); + response.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Length': String(body.length), + }); + response.end(body); + return; + } + + response.writeHead(404, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 404, msg: 'unexpected request' })); + }); + + try { + bundle = await buildCliWithEnv({ + CAR_API_BASE: server.baseUrl, + }); + await writeAuthConfig(temp.home); + const result = await runCli( + ['export', cid, '--output', outputDir], + { + home: temp.home, + cliPath: bundle.cliPath, + timeout: 20000, + }, + ); + + expect(result.exitCode, outputOf(result)).toBe(0); + const output = outputOf(result); + expect(output).toContain('Export task created: export-task-1'); + expect(output).toContain('Export successful'); + expect(output).toContain(`CID: ${cid}`); + await expect(readFile(path.join(outputDir, `${cid}.car`), 'utf8')) + .resolves.toBe('fixture car payload'); + expect(server.requests.map((request) => request.url)).toEqual([ + `/car/export?cid=${cid}&uid=0x1234567890abcdef`, + '/car/export/status?task_id=export-task-1', + `/downloads/${cid}.car`, + ]); + } finally { + if (bundle) { + await bundle.cleanup(); + } + await server.close(); + await temp.cleanup(); + } + }); + + test('create scaffolds a project, deploys worker, and uploads frontend dist', async () => { + const temp = await createTempHome(); + let bundle: Awaited> | undefined; + const templateZip = createTemplateZipBuffer(); + const server = await startLocalHttpServer((request, response) => { + const bodyText = request.body.toString('utf8'); + + if (request.method === 'POST' && request.url === '/create_worker') { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(JSON.parse(bodyText)).toEqual({ + project_name: 'demo-create', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + api_domain: 'https://api.demo-create.pinme.test', + metadata: { + project_name: 'demo-create', + bindings: [ + { name: 'API_KEY', text: 'real-api-key' }, + { name: 'PROJECT_NAME', text: 'demo-create' }, + ], + }, + project_name: 'demo-create', + uuid: 'worker-uuid-1', + api_key: 'real-api-key', + public_client_config: { + auth_api_key: 'public-auth-key', + auth_domain: 'auth.pinme.test', + auth_project_id: 'pinme-project', + tenant_id: 'tenant-1', + }, + }, + }), + ); + return; + } + + if (request.method === 'GET' && request.url === '/template.zip') { + response.writeHead(200, { + 'Content-Type': 'application/zip', + 'Content-Length': String(templateZip.length), + }); + response.end(templateZip); + return; + } + + if ( + request.method === 'PUT' && + request.url === '/save_worker?project_name=demo-create' + ) { + expect(request.headers['token-address']).toBe('0x1234567890abcdef'); + expect(request.headers['authentication-tokens']).toBe('test-token'); + expect(bodyText).toContain('metadata.json'); + expect(bodyText).toContain('worker.js'); + expect(bodyText).toContain('001_init.sql'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + sql_results: [ + { filename: '001_init.sql', status: 'complete' }, + ], + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/init') { + expect(JSON.parse(bodyText)).toMatchObject({ + is_directory: true, + uid: '0x1234567890abcdef', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + session_id: 'create-session-1', + total_chunks: 1, + chunk_size: 1024 * 1024, + }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/upload') { + expect(bodyText).toContain('create-session-1'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { chunk_index: 0, chunk_size: request.body.length }, + }), + ); + return; + } + + if (request.method === 'POST' && request.url === '/chunk/complete') { + expect(JSON.parse(bodyText)).toMatchObject({ + session_id: 'create-session-1', + uid: '0x1234567890abcdef', + action: 'project_create', + project_name: 'demo-create', + }); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { trace_id: 'create-trace-1' }, + }), + ); + return; + } + + if ( + request.method === 'GET' && + request.url === + '/up_status?trace_id=create-trace-1&uid=0x1234567890abcdef&project_name=demo-create' + ) { + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 200, + data: { + is_ready: true, + upload_rst: { + Bytes: 64, + Name: 'dist', + Size: 64, + Hash: 'bafy-create-frontend', + }, + }, + }), + ); + return; + } + + response.writeHead(404, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify({ code: 404, msg: 'unexpected request' })); + }); + + try { + const fakeBin = await createFakeNpmBin(temp.home); + bundle = await buildCliWithEnv({ + PINME_API_BASE: server.baseUrl, + IPFS_API_URL: server.baseUrl, + PINME_TEMPLATE_ZIP_URL: `${server.baseUrl}/template.zip`, + POLL_INTERVAL_SECONDS: '0', + MAX_POLL_TIME_MINUTES: '1', + }); + await writeAuthConfig(temp.home); + const result = await runCli(['create', 'Demo-Create', '--force'], { + home: temp.home, + cwd: temp.home, + cliPath: bundle.cliPath, + timeout: 25000, + env: { + PATH: `${fakeBin}:${process.env.PATH || ''}`, + }, + }); + + expect(result.exitCode, outputOf(result)).toBe(0); + const output = outputOf(result); + expect(output).toContain('Project created successfully.'); + expect(output).toContain('Worker deployed'); + expect(output).toContain('Frontend URL'); + expect(output).toContain('Project Management URL'); + + const projectDir = path.join(temp.home, 'Demo-Create'); + await expect(readFile(path.join(projectDir, 'pinme.toml'), 'utf8')) + .resolves.toContain('project_name = "demo-create"'); + await expect(readFile(path.join(projectDir, 'pinme.toml'), 'utf8')) + .resolves.toContain( + 'api_url = "https://api.demo-create.pinme.test"', + ); + await expect(readFile(path.join(projectDir, 'pinme.toml'), 'utf8')) + .resolves.toContain( + 'frontend_url = "https://project.pinme.test/demo-create"', + ); + await expect( + readFile(path.join(projectDir, 'backend', 'metadata.json'), 'utf8'), + ).resolves.toContain('"PROJECT_NAME"'); + await expect( + readFile( + path.join(projectDir, 'frontend', 'src', 'utils', 'config.ts'), + 'utf8', + ), + ).resolves.toContain('public_client_config'); + await expect( + readFile(path.join(projectDir, 'frontend', 'dist', 'index.html'), 'utf8'), + ).resolves.toContain('https://api.demo-create.pinme.test'); + expect(server.requests.map((request) => request.url)).toEqual([ + '/create_worker', + '/template.zip', + '/save_worker?project_name=demo-create', + '/chunk/init', + '/chunk/upload', + '/chunk/complete', + '/up_status?trace_id=create-trace-1&uid=0x1234567890abcdef&project_name=demo-create', + ]); + } finally { + if (bundle) { + await bundle.cleanup(); + } + await server.close(); + await temp.cleanup(); + } + }); +}); diff --git a/test/fixtures/project/db/001_init.sql b/test/fixtures/project/db/001_init.sql new file mode 100644 index 0000000..c5ef6d1 --- /dev/null +++ b/test/fixtures/project/db/001_init.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL +); diff --git a/test/fixtures/project/dist/index.html b/test/fixtures/project/dist/index.html new file mode 100644 index 0000000..85aa6a4 --- /dev/null +++ b/test/fixtures/project/dist/index.html @@ -0,0 +1,6 @@ + + + + Fixture project dist + + diff --git a/test/fixtures/project/pinme.toml b/test/fixtures/project/pinme.toml new file mode 100644 index 0000000..a70d374 --- /dev/null +++ b/test/fixtures/project/pinme.toml @@ -0,0 +1 @@ +project_name = "fixture-project" diff --git a/test/fixtures/site/index.html b/test/fixtures/site/index.html new file mode 100644 index 0000000..49e6d53 --- /dev/null +++ b/test/fixtures/site/index.html @@ -0,0 +1,10 @@ + + + + + PinMe fixture + + +

Fixture site

+ + diff --git a/test/helpers/cliRunner.ts b/test/helpers/cliRunner.ts new file mode 100644 index 0000000..5cbb298 --- /dev/null +++ b/test/helpers/cliRunner.ts @@ -0,0 +1,108 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; +import { mkdir, rm, writeFile } from 'fs/promises'; +import { execaNode } from 'execa'; +import { dir } from 'tmp-promise'; +import { build } from 'esbuild'; +import packageJson from '../../package.json'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +export const repoRoot = path.resolve(__dirname, '..', '..'); +export const cliEntry = path.join(repoRoot, 'dist', 'index.js'); + +export interface TempHome { + home: string; + cleanup: () => Promise; +} + +export async function createTempHome(): Promise { + const temp = await dir({ + prefix: 'pinme-cli-home-', + unsafeCleanup: true, + }); + + return { + home: temp.path, + cleanup: temp.cleanup, + }; +} + +export async function writeAuthConfig(home: string): Promise { + const configDir = path.join(home, '.pinme'); + await mkdir(configDir, { recursive: true }); + await writeFile( + path.join(configDir, 'auth.json'), + JSON.stringify( + { + address: '0x1234567890abcdef', + token: 'test-token', + }, + null, + 2, + ), + ); +} + +export function runCli( + args: string[], + options: { + home: string; + cwd?: string; + timeout?: number; + env?: Record; + cliPath?: string; + }, +): any { + return execaNode(options.cliPath || cliEntry, args, { + cwd: options.cwd || repoRoot, + reject: false, + timeout: options.timeout || 15000, + env: { + HOME: options.home, + USERPROFILE: options.home, + PINME_TRACKING_DISABLED: '1', + PINME_API_BASE: 'http://127.0.0.1:9', + IPFS_API_URL: 'http://127.0.0.1:9', + CAR_API_BASE: 'http://127.0.0.1:9', + IPFS_PREVIEW_URL: 'https://preview.pinme.test/#/preview/', + PROJECT_PREVIEW_URL: 'https://project.pinme.test/', + PINME_WEB_URL: 'https://app.pinme.test', + ...options.env, + }, + }); +} + +export async function buildCliWithEnv( + env: Record, +): Promise<{ cliPath: string; cleanup: () => Promise }> { + const outfile = path.join( + repoRoot, + 'dist', + `.test-cli-${process.pid}-${Date.now()}.js`, + ); + const define: Record = {}; + + for (const [key, value] of Object.entries(env)) { + define[`process.env.${key}`] = JSON.stringify(value); + } + + await build({ + entryPoints: [path.join(repoRoot, 'bin', 'index.ts')], + outfile, + bundle: true, + platform: 'node', + target: 'node14', + format: 'cjs', + external: Object.keys(packageJson.dependencies || {}).filter( + (dependency) => dependency !== 'axios', + ), + banner: { js: '#!/usr/bin/env node' }, + logLevel: 'silent', + define, + }); + + return { + cliPath: outfile, + cleanup: () => rm(outfile, { force: true }), + }; +} diff --git a/test/helpers/localHttpServer.ts b/test/helpers/localHttpServer.ts new file mode 100644 index 0000000..16159fa --- /dev/null +++ b/test/helpers/localHttpServer.ts @@ -0,0 +1,75 @@ +import http, { type IncomingMessage, type ServerResponse } from 'http'; + +export interface RecordedRequest { + method: string; + url: string; + headers: http.IncomingHttpHeaders; + body: Buffer; +} + +export interface LocalHttpServer { + baseUrl: string; + requests: RecordedRequest[]; + close: () => Promise; +} + +export async function startLocalHttpServer( + handler: ( + request: RecordedRequest, + response: ServerResponse, + ) => void | Promise, +): Promise { + const requests: RecordedRequest[] = []; + const server = http.createServer( + async (request: IncomingMessage, response: ServerResponse) => { + const chunks: Buffer[] = []; + request.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + request.on('end', async () => { + const recorded: RecordedRequest = { + method: request.method || 'GET', + url: request.url || '/', + headers: request.headers, + body: Buffer.concat(chunks), + }; + requests.push(recorded); + + try { + await handler(recorded, response); + } catch (error: any) { + response.writeHead(500, { 'Content-Type': 'application/json' }); + response.end( + JSON.stringify({ + code: 500, + msg: error?.message || 'local test server error', + }), + ); + } + }); + }, + ); + + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.off('error', reject); + resolve(); + }); + }); + + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Failed to start local HTTP server'); + } + + return { + baseUrl: `http://127.0.0.1:${address.port}`, + requests, + close: () => + new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + }), + }; +} diff --git a/test/integration/apiClient.test.ts b/test/integration/apiClient.test.ts new file mode 100644 index 0000000..03bfdf4 --- /dev/null +++ b/test/integration/apiClient.test.ts @@ -0,0 +1,233 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import nock from 'nock'; + +async function loadApiClient(env: Record = {}) { + vi.resetModules(); + vi.doUnmock('../../bin/utils/webLogin'); + process.env.PINME_API_BASE = env.PINME_API_BASE || 'https://api.pinme.test'; + return import('../../bin/utils/apiClient'); +} + +async function loadApiClientWithAuthMock() { + vi.resetModules(); + process.env.PINME_API_BASE = 'https://api.pinme.test'; + vi.doMock('../../bin/utils/webLogin', () => ({ + getAuthHeaders: () => ({ + 'token-address': '0xabc', + 'authentication-tokens': 'secret-token', + }), + })); + return import('../../bin/utils/apiClient'); +} + +async function loadApiClientWithThrowingAuthMock() { + vi.resetModules(); + process.env.PINME_API_BASE = 'https://api.pinme.test'; + vi.doMock('../../bin/utils/webLogin', () => ({ + getAuthHeaders: () => { + throw new Error('auth config unreadable'); + }, + })); + return import('../../bin/utils/apiClient'); +} + +describe('apiClient', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + test('returns successful business responses', async () => { + const { createPinmeApiClient } = await loadApiClient(); + nock('https://api.pinme.test') + .get('/root_domain') + .reply(200, { code: 200, data: { domain: 'pinme.test' } }); + + const response = await createPinmeApiClient().get('/root_domain'); + + expect(response.data.data.domain).toBe('pinme.test'); + expect(nock.isDone()).toBe(true); + }); + + test('turns non-200 business codes into CliError', async () => { + const { createPinmeApiClient } = await loadApiClient(); + nock('https://api.pinme.test') + .post('/bind_pinme_domain') + .reply(200, { code: 500, msg: 'Domain is taken' }); + + await expect( + createPinmeApiClient().post('/bind_pinme_domain', { + domain_name: 'demo', + hash: 'bafy', + }), + ).rejects.toMatchObject({ + name: 'CliError', + stage: 'API request', + message: 'Domain is taken', + details: ['Request: POST /bind_pinme_domain', 'Business code: 500'], + }); + }); + + test('wraps HTTP failures with request context', async () => { + const { createPinmeApiClient } = await loadApiClient(); + nock('https://api.pinme.test') + .get('/my_domains') + .reply(503, { message: 'Temporarily unavailable' }); + + await expect(createPinmeApiClient().get('/my_domains')).rejects.toMatchObject( + { + name: 'CliError', + stage: 'API request', + message: 'Temporarily unavailable', + details: ['Request: GET /my_domains', 'HTTP status: 503'], + }, + ); + }); + + test('injects auth headers from local auth config by default', async () => { + const { createPinmeApiClient } = await loadApiClientWithAuthMock(); + let capturedHeaders: Record = {}; + nock('https://api.pinme.test') + .get('/my_domains') + .reply(function () { + capturedHeaders = this.req.headers; + return [200, { code: 200, data: [] }]; + }); + + await expect(createPinmeApiClient().get('/my_domains')).resolves.toMatchObject( + { + data: { code: 200, data: [] }, + }, + ); + expect(capturedHeaders['token-address']).toBe('0xabc'); + expect(capturedHeaders['authentication-tokens']).toBe('secret-token'); + }); + + test('omits auth headers when includeAuth is false', async () => { + const { createPinmeApiClient } = await loadApiClientWithAuthMock(); + nock('https://api.pinme.test', { + badheaders: ['token-address', 'authentication-tokens'], + }) + .get('/public') + .reply(200, { ok: true }); + + const response = await createPinmeApiClient({ includeAuth: false }).get( + '/public', + ); + + expect(response.data).toEqual({ ok: true }); + }); + + test('merges custom headers over defaults', async () => { + const { createPinmeApiClient } = await loadApiClient(); + nock('https://api.pinme.test', { + reqheaders: { + 'user-agent': 'Custom-Agent', + 'x-test-suite': 'api-client', + }, + }) + .get('/headers') + .reply(200, { ok: true }); + + await createPinmeApiClient({ + includeAuth: false, + headers: { + 'User-Agent': 'Custom-Agent', + 'X-Test-Suite': 'api-client', + }, + }).get('/headers'); + + expect(nock.isDone()).toBe(true); + }); + + test('sends default JSON CLI headers', async () => { + const { createPinmeApiClient } = await loadApiClient(); + let capturedHeaders: Record = {}; + nock('https://api.pinme.test') + .get('/headers') + .reply(function () { + capturedHeaders = this.req.headers; + return [200, { ok: true }]; + }); + + await createPinmeApiClient({ includeAuth: false }).get('/headers'); + + expect(capturedHeaders.accept).toBe('*/*'); + expect(capturedHeaders['content-type']).toBe('application/json'); + expect(capturedHeaders['user-agent']).toBe('Pinme-CLI'); + expect(capturedHeaders.connection).toBe('keep-alive'); + }); + + test('continues without auth headers when local auth config is unreadable', async () => { + const { createPinmeApiClient } = await loadApiClientWithThrowingAuthMock(); + nock('https://api.pinme.test', { + badheaders: ['token-address', 'authentication-tokens'], + }) + .get('/public') + .reply(200, { ok: true }); + + await expect(createPinmeApiClient().get('/public')).resolves.toMatchObject({ + data: { ok: true }, + }); + }); + + test('does not treat non-object response bodies as business errors', async () => { + const { createPinmeApiClient } = await loadApiClient(); + nock('https://api.pinme.test').get('/plain').reply(200, 'code 500'); + + const response = await createPinmeApiClient({ includeAuth: false }).get( + '/plain', + ); + + expect(response.data).toBe('code 500'); + }); + + test('omits request context when axios config has no URL', async () => { + const { createApiClient } = await loadApiClient(); + const client = createApiClient({ + includeAuth: false, + baseURL: 'https://api.pinme.test', + }); + + await expect( + client.request({ + adapter: async (config) => ({ + data: { code: 500, msg: 'Adapter business failure' }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }), + ).rejects.toMatchObject({ + message: 'Adapter business failure', + details: ['Business code: 500'], + }); + }); + + test('uses GET as the default request descriptor method', async () => { + const { createApiClient } = await loadApiClient(); + const client = createApiClient({ + includeAuth: false, + baseURL: 'https://api.pinme.test', + }); + + await expect( + client.request({ + url: '/adapter-default-method', + adapter: async (config) => ({ + data: { code: 500, msg: 'Adapter business failure' }, + status: 200, + statusText: 'OK', + headers: {}, + config, + }), + }), + ).rejects.toMatchObject({ + message: 'Adapter business failure', + details: [ + 'Request: GET /adapter-default-method', + 'Business code: 500', + ], + }); + }); +}); diff --git a/test/integration/pinmeApi.test.ts b/test/integration/pinmeApi.test.ts new file mode 100644 index 0000000..342252b --- /dev/null +++ b/test/integration/pinmeApi.test.ts @@ -0,0 +1,491 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import nock from 'nock'; + +async function loadPinmeApi(env: Record) { + vi.resetModules(); + Object.assign(process.env, env); + return import('../../bin/utils/pinmeApi'); +} + +describe('pinmeApi', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + test('returns DNS domains as available without an API call', async () => { + const { checkDomainAvailable } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + + await expect(checkDomainAvailable('example.com')).resolves.toEqual({ + is_valid: true, + }); + expect(nock.isDone()).toBe(true); + }); + + test('checks PinMe subdomain availability through configured endpoint', async () => { + const { checkDomainAvailable } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + PINME_CHECK_DOMAIN_PATH: '/check_domain', + }); + nock('https://api.pinme.test') + .post('/check_domain', { domain_name: 'demo' }) + .reply(200, { data: { is_valid: false, error: 'taken' } }); + + await expect(checkDomainAvailable('demo')).resolves.toEqual({ + is_valid: false, + error: 'taken', + }); + }); + + test('defaults subdomain availability to true for unexpected successful shapes', async () => { + const { checkDomainAvailable } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + PINME_CHECK_DOMAIN_PATH: '/check_domain', + }); + nock('https://api.pinme.test') + .post('/check_domain', { domain_name: 'demo' }) + .reply(200, { code: 200, data: { unexpected: true } }); + + await expect(checkDomainAvailable('demo')).resolves.toEqual({ + is_valid: true, + }); + }); + + test('checks top-level domain availability and surfaces recoverable HTTP failures', async () => { + const { checkDomainAvailable } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + PINME_CHECK_DOMAIN_PATH: '/check_domain', + }); + nock('https://api.pinme.test') + .post('/check_domain', { domain_name: 'direct' }) + .reply(200, { is_valid: false, error: 'reserved' }) + .post('/check_domain', { domain_name: 'missing' }) + .reply(404, { message: 'not found' }); + + await expect(checkDomainAvailable('direct')).resolves.toEqual({ + is_valid: false, + error: 'reserved', + }); + await expect(checkDomainAvailable('missing')).rejects.toThrow('not found'); + }); + + test('caches root domain until force refresh', async () => { + const { getRootDomain } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .get('/root_domain') + .reply(200, { code: 200, data: { domain: 'first.pinme.test' } }) + .get('/root_domain') + .reply(200, { code: 200, data: { domain: 'second.pinme.test' } }); + + await expect(getRootDomain()).resolves.toBe('first.pinme.test'); + await expect(getRootDomain()).resolves.toBe('first.pinme.test'); + await expect(getRootDomain(true)).resolves.toBe('second.pinme.test'); + }); + + test('getRootDomain rejects successful responses without a domain', async () => { + const { getRootDomain } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .get('/root_domain') + .reply(200, { code: 200, msg: 'domain missing', data: {} }); + + await expect(getRootDomain(true)).rejects.toThrow('domain missing'); + }); + + test('binds anonymous devices and returns false on token expiration', async () => { + const { bindAnonymousDevice } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .post('/bind_anonymous', { anonymous_uid: 'anon-1' }) + .reply(200, { code: 200 }) + .post('/bind_anonymous', { anonymous_uid: 'anon-2' }) + .reply(401, { message: 'token expired' }); + + await expect(bindAnonymousDevice('anon-1')).resolves.toBe(true); + await expect(bindAnonymousDevice('anon-2')).resolves.toBe(false); + }); + + test('bindAnonymousDevice returns false for non-token failures', async () => { + const { bindAnonymousDevice } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .post('/bind_anonymous', { anonymous_uid: 'anon-fail' }) + .reply(500, { message: 'server exploded' }); + + await expect(bindAnonymousDevice('anon-fail')).resolves.toBe(false); + }); + + test('throws token expired for auth failures', async () => { + const { getMyDomains } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .get('/my_domains') + .reply(401, { message: 'token expired' }); + + await expect(getMyDomains()).rejects.toThrow('Token expired'); + }); + + test('detects token expiration from business codes and localized messages', async () => { + const { checkDomainAvailable, getMyDomains } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + PINME_CHECK_DOMAIN_PATH: '/check_domain', + }); + nock('https://api.pinme.test') + .post('/check_domain', { domain_name: 'demo' }) + .reply(200, { code: 10001, msg: '登录已过期' }) + .get('/my_domains') + .reply(200, { code: 'TOKEN_EXPIRED', msg: 'token expired' }); + + await expect(checkDomainAvailable('demo')).rejects.toThrow( + 'Token expired', + ); + await expect(getMyDomains()).rejects.toThrow('Token expired'); + }); + + test('reads domain list variants and returns empty arrays for business failures', async () => { + const { getMyDomains } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .get('/my_domains') + .reply(200, { + code: 200, + data: { + list: [ + { + domain_name: 'demo', + domain_type: 1, + bind_time: 1, + expire_time: 2, + }, + ], + }, + }) + .get('/my_domains') + .reply(200, { msg: 'missing code' }); + + await expect(getMyDomains()).resolves.toHaveLength(1); + await expect(getMyDomains()).resolves.toEqual([]); + }); + + test('reads array domain lists and treats business auth codes as expired', async () => { + const { getMyDomains } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .get('/my_domains') + .reply(200, { + code: 200, + data: [ + { + domain_name: 'array-demo', + domain_type: 1, + bind_time: 1, + expire_time: 2, + }, + ], + }) + .get('/my_domains') + .reply(200, { code: 403, msg: 'auth failed' }); + + await expect(getMyDomains()).resolves.toEqual([ + expect.objectContaining({ domain_name: 'array-demo' }), + ]); + await expect(getMyDomains()).rejects.toThrow('Token expired'); + }); + + test('getMyDomains handles unsupported payloads and both auth business codes', async () => { + const { getMyDomains } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .get('/my_domains') + .reply(200, { code: 200, data: { list: 'not an array' } }) + .get('/my_domains') + .reply(200, { code: 401, msg: 'auth failed' }); + + await expect(getMyDomains()).resolves.toEqual([]); + await expect(getMyDomains()).rejects.toThrow('Token expired'); + }); + + test('binds DNS domains with auth headers', async () => { + const { bindDnsDomainV4 } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test', { + reqheaders: { + 'x-auth-token': 'token', + 'x-token-address': '0xabc', + }, + }) + .post('/bind_dns', { domain_name: 'example.com', hash: 'bafy' }) + .reply(200, { + code: 200, + msg: 'ok', + data: { domain_name: 'example.com', hash: 'bafy' }, + }); + + await expect( + bindDnsDomainV4('example.com', 'bafy', '0xabc', 'token'), + ).resolves.toMatchObject({ + code: 200, + data: { domain_name: 'example.com' }, + }); + }); + + test('binds PinMe subdomains and reports wallet balance and VIP status', async () => { + const { bindPinmeDomain, getWalletBalance, isVip } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .post('/bind_pinme_domain', { + domain_name: 'demo', + hash: 'bafy', + project_name: 'project', + }) + .reply(200, { code: 200 }) + .get('/pay/wallet/balance') + .reply(200, { + code: 200, + msg: 'ok', + data: { wallet_balance_usd: 12.5 }, + }) + .get('/is_vip') + .reply(200, { + code: 200, + msg: 'ok', + data: { is_vip: true }, + }); + + await expect(bindPinmeDomain('demo', 'bafy', 'project')).resolves.toBe(true); + await expect(getWalletBalance('0xabc', 'token')).resolves.toMatchObject({ + data: { wallet_balance_usd: 12.5 }, + }); + await expect(isVip('0xabc', 'token')).resolves.toMatchObject({ + data: { is_vip: true }, + }); + }); + + test('bindPinmeDomain returns false for successful responses without code 200', async () => { + const { bindPinmeDomain } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .post('/bind_pinme_domain', { + domain_name: 'demo', + hash: 'bafy', + }) + .reply(200, { msg: 'missing code' }) + .post('/bind_pinme_domain', { + domain_name: 'project-demo', + hash: 'bafy-project', + project_name: 'project', + }) + .reply(200, { code: 200, msg: 'ok' }); + + await expect(bindPinmeDomain('demo', 'bafy')).resolves.toBe(false); + await expect( + bindPinmeDomain('project-demo', 'bafy-project', 'project'), + ).resolves.toBe(true); + }); + + test('sends account auth headers for wallet and VIP requests', async () => { + const { getWalletBalance, isVip } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test', { + reqheaders: { + 'authentication-tokens': 'wallet-token', + 'token-address': '0xwallet', + }, + }) + .get('/pay/wallet/balance') + .reply(200, { code: 200, msg: 'ok', data: { wallet_balance_usd: 1 } }); + nock('https://api.pinme.test', { + reqheaders: { + 'x-auth-token': 'vip-token', + 'x-token-address': '0xvip', + }, + }) + .get('/is_vip') + .reply(200, { code: 200, msg: 'ok', data: { is_vip: false } }); + + await expect(getWalletBalance('0xwallet', 'wallet-token')).resolves.toEqual( + { + code: 200, + msg: 'ok', + data: { wallet_balance_usd: 1 }, + }, + ); + await expect(isVip('0xvip', 'vip-token')).resolves.toEqual({ + code: 200, + msg: 'ok', + data: { is_vip: false }, + }); + }); + + test('surfaces token expiration from bind and account APIs', async () => { + const { bindPinmeDomain, bindDnsDomainV4, getWalletBalance, isVip } = + await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + }); + nock('https://api.pinme.test') + .post('/bind_pinme_domain') + .reply(403, { message: 'invalid token' }) + .post('/bind_dns') + .reply(401, { message: 'token expired' }) + .get('/pay/wallet/balance') + .reply(401, { message: 'unauthorized' }) + .get('/is_vip') + .reply(401, { message: 'auth failed' }); + + await expect(bindPinmeDomain('demo', 'bafy')).rejects.toThrow( + 'Token expired', + ); + await expect( + bindDnsDomainV4('example.com', 'bafy', '0xabc', 'token'), + ).rejects.toThrow('Token expired'); + await expect(getWalletBalance('0xabc', 'token')).rejects.toThrow( + 'Token expired', + ); + await expect(isVip('0xabc', 'token')).rejects.toThrow('Token expired'); + }); + + test('requests CAR export through the CAR API client', async () => { + const { requestCarExport } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + CAR_API_BASE: 'https://car.pinme.test', + }); + nock('https://car.pinme.test') + .post('/car/export') + .query({ cid: 'bafy', uid: 'uid-1' }) + .reply(200, { + code: 200, + msg: 'ok', + data: { + cid: 'bafy', + status: 'processing', + task_id: 'task-1', + }, + }); + + await expect(requestCarExport('bafy', 'uid-1')).resolves.toEqual({ + cid: 'bafy', + status: 'processing', + task_id: 'task-1', + }); + }); + + test('checks CAR export status and normalizes API failures', async () => { + const { checkCarExportStatus, requestCarExport } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + CAR_API_BASE: 'https://car.pinme.test', + }); + nock('https://car.pinme.test') + .get('/car/export/status') + .query({ task_id: 'task-1' }) + .reply(200, { + code: 200, + msg: 'ok', + data: { + task_id: 'task-1', + cid: 'bafy', + status: 'completed', + download_url: 'https://download.pinme.test/file.car', + }, + }) + .post('/car/export') + .query({ cid: 'bad', uid: 'uid-1' }) + .reply(200, { code: 500, msg: 'Export failed' }); + + await expect(checkCarExportStatus('task-1')).resolves.toMatchObject({ + status: 'completed', + download_url: 'https://download.pinme.test/file.car', + }); + await expect(requestCarExport('bad', 'uid-1')).rejects.toThrow( + /Failed to request CAR export: Export failed|Export failed/, + ); + }); + + test('rejects CAR success codes that omit payload data', async () => { + const { checkCarExportStatus, requestCarExport } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + CAR_API_BASE: 'https://car.pinme.test', + }); + nock('https://car.pinme.test') + .post('/car/export') + .query({ cid: 'empty', uid: 'uid-1' }) + .reply(200, { code: 200, msg: 'missing export data' }) + .get('/car/export/status') + .query({ task_id: 'empty-task' }) + .reply(200, { code: 200, msg: 'missing status data' }); + + await expect(requestCarExport('empty', 'uid-1')).rejects.toThrow( + /missing export data/, + ); + await expect(checkCarExportStatus('empty-task')).rejects.toThrow( + /missing status data/, + ); + }); + + test('normalizes CAR status token expiration and response messages', async () => { + const { checkCarExportStatus } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + CAR_API_BASE: 'https://car.pinme.test', + }); + nock('https://car.pinme.test') + .get('/car/export/status') + .query({ task_id: 'expired' }) + .reply(401, { message: 'token expired' }) + .get('/car/export/status') + .query({ task_id: 'failed' }) + .reply(200, { code: 500, msg: 'Task failed' }); + + await expect(checkCarExportStatus('expired')).rejects.toThrow( + 'Token expired', + ); + await expect(checkCarExportStatus('failed')).rejects.toThrow( + /Failed to check export status: Task failed|Task failed/, + ); + }); + + test('normalizes CAR HTTP response messages and generic failures', async () => { + const { checkCarExportStatus, requestCarExport } = await loadPinmeApi({ + PINME_API_BASE: 'https://api.pinme.test', + CAR_API_BASE: 'https://car.pinme.test', + }); + nock('https://car.pinme.test') + .post('/car/export') + .query({ cid: 'http-fail', uid: 'uid-1' }) + .reply(503, { msg: 'CAR export unavailable' }) + .post('/car/export') + .query({ cid: 'network-fail', uid: 'uid-1' }) + .replyWithError('socket closed') + .get('/car/export/status') + .query({ task_id: 'http-fail' }) + .reply(500, { msg: 'status unavailable' }) + .get('/car/export/status') + .query({ task_id: 'network-fail' }) + .replyWithError('status socket closed'); + + await expect(requestCarExport('http-fail', 'uid-1')).rejects.toThrow( + 'CAR export unavailable', + ); + await expect(requestCarExport('network-fail', 'uid-1')).rejects.toThrow( + /Failed to request CAR export: socket closed/, + ); + await expect(checkCarExportStatus('http-fail')).rejects.toThrow( + 'status unavailable', + ); + await expect(checkCarExportStatus('network-fail')).rejects.toThrow( + /Failed to check export status: status socket closed/, + ); + }); +}); diff --git a/test/login-tracking-source.test.mjs b/test/login-tracking-source.test.mjs new file mode 100644 index 0000000..3e2d648 --- /dev/null +++ b/test/login-tracking-source.test.mjs @@ -0,0 +1,9 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "vitest"; + +const loginSource = readFileSync(new URL("../bin/login.ts", import.meta.url), "utf8"); + +test("pinme login tracking sends login method through source", () => { + assert.match(loginSource, /source:\s*['"]cli['"]/); +}); diff --git a/test/pack/npmPack.test.ts b/test/pack/npmPack.test.ts new file mode 100644 index 0000000..fd44694 --- /dev/null +++ b/test/pack/npmPack.test.ts @@ -0,0 +1,75 @@ +import path from 'path'; +import { mkdir, readFile } from 'fs/promises'; +import { describe, expect, test } from 'vitest'; +import { execa } from 'execa'; +import { dir } from 'tmp-promise'; +import { cliEntry, repoRoot } from '../helpers/cliRunner'; + +describe('npm package', () => { + test('build output has a shebang and package bin points to it', async () => { + const [entry, packageJsonRaw] = await Promise.all([ + readFile(cliEntry, 'utf8'), + readFile(path.join(repoRoot, 'package.json'), 'utf8'), + ]); + const packageJson = JSON.parse(packageJsonRaw); + + expect(entry.split('\n')[0]).toBe('#!/usr/bin/env node'); + expect(packageJson.bin.pinme).toBe('./dist/index.js'); + }); + + test( + 'npm pack includes the CLI bundle and installable bin', + async () => { + const temp = await dir({ + prefix: 'pinme-pack-', + unsafeCleanup: true, + }); + + try { + const npmCache = path.join(temp.path, 'npm-cache'); + const pack = await execa( + 'npm', + ['pack', '--json', '--pack-destination', temp.path], + { + cwd: repoRoot, + env: { + npm_config_cache: npmCache, + }, + }, + ); + const [packed] = JSON.parse(pack.stdout); + const files = packed.files.map((file: { path: string }) => file.path); + const tarball = path.join(temp.path, packed.filename); + + expect(files.sort()).toEqual([ + 'LICENSE', + 'README.md', + 'dist/index.js', + 'package.json', + ]); + + const extractDir = path.join(temp.path, 'extract'); + await mkdir(extractDir); + await execa('tar', ['-xzf', tarball, '-C', extractDir]); + + const extractedPackageJson = JSON.parse( + await readFile(path.join(extractDir, 'package', 'package.json'), 'utf8'), + ); + const extractedEntry = await readFile( + path.join(extractDir, 'package', 'dist', 'index.js'), + 'utf8', + ); + + expect(extractedPackageJson.bin.pinme).toBe('./dist/index.js'); + expect(extractedEntry.split('\n')[0]).toBe('#!/usr/bin/env node'); + await execa('node', [ + '--check', + path.join(extractDir, 'package', 'dist', 'index.js'), + ]); + } finally { + await temp.cleanup(); + } + }, + 30000, + ); +}); diff --git a/test/setup/nock.ts b/test/setup/nock.ts new file mode 100644 index 0000000..61b0986 --- /dev/null +++ b/test/setup/nock.ts @@ -0,0 +1,15 @@ +import { afterAll, afterEach, beforeAll } from 'vitest'; +import nock from 'nock'; + +beforeAll(() => { + nock.disableNetConnect(); + nock.enableNetConnect((host) => host.startsWith('127.0.0.1')); +}); + +afterEach(() => { + nock.cleanAll(); +}); + +afterAll(() => { + nock.enableNetConnect(); +}); diff --git a/test/unit/cliError.test.ts b/test/unit/cliError.test.ts new file mode 100644 index 0000000..a04608c --- /dev/null +++ b/test/unit/cliError.test.ts @@ -0,0 +1,347 @@ +import { describe, expect, test, vi } from 'vitest'; +import { + CliError, + createApiError, + createConfigError, + createCommandError, + normalizeCliError, + printCliError, + printRechargeUrl, +} from '../../bin/utils/cliError'; + +describe('cliError', () => { + test('CliError constructor defaults details and suggestions', () => { + const cause = new Error('root cause'); + const error = new CliError({ + summary: 'Plain failure.', + cause, + }); + + expect(error.message).toBe('Plain failure.'); + expect(error.name).toBe('CliError'); + expect(error.stage).toBeUndefined(); + expect(error.details).toEqual([]); + expect(error.suggestions).toEqual([]); + expect(error.cause).toBe(cause); + }); + + test('normalizes API business errors with request context', () => { + const error = createApiError( + 'API request', + { + response: { + data: { + code: 40001, + msg: 'Insufficient wallet balance', + }, + }, + }, + ['Request: POST /bind_dns'], + ); + + expect(error).toBeInstanceOf(CliError); + expect(error.message).toBe('Insufficient wallet balance'); + expect(error.details).toContain('Request: POST /bind_dns'); + expect(error.details).toContain('Business code: 40001'); + expect(error.details.join('\n')).toMatch(/Recharge URL:/); + }); + + test('prefers nested API messages in documented order', () => { + expect( + createApiError('API request', { + response: { + data: { + data: { + message: 'Nested message', + error: 'Nested error', + }, + errors: [{ message: 'Array message' }], + error: 'Top-level error', + }, + }, + }).message, + ).toBe('Nested message'); + + expect( + createApiError('API request', { + response: { + data: { + errors: [{ message: 'Array message' }], + error: 'Top-level error', + }, + }, + }).message, + ).toBe('Array message'); + }); + + test('normalizes HTTP errors without business code', () => { + const error = createApiError('API request', { + response: { + status: 503, + data: { message: 'Service unavailable' }, + }, + message: 'Request failed with status code 503', + }); + + expect(error.message).toBe('Service unavailable'); + expect(error.details).toContain('HTTP status: 503'); + expect(error.details.join('\n')).not.toMatch(/Reason:/); + }); + + test('creates command errors with exit metadata', () => { + const error = createCommandError( + 'frontend build', + 'npm run build:web', + { + status: 127, + message: 'vite: command not found', + }, + ['Run npm install'], + ); + + expect(error.details).toEqual([ + 'Command: npm run build:web', + 'Exit code: 127', + 'Reason: vite: command not found', + ]); + expect(error.suggestions).toEqual(['Run npm install']); + }); + + test('creates command errors with code and signal metadata without suggestions', () => { + const error = createCommandError('worker deploy', 'wrangler deploy', { + code: 1, + signal: 'SIGTERM', + }); + + expect(error.message).toBe('worker deploy failed.'); + expect(error.details).toEqual([ + 'Command: wrangler deploy', + 'Exit code: 1', + 'Signal: SIGTERM', + ]); + expect(error.suggestions).toEqual([]); + }); + + test('normalizes unknown thrown values', () => { + const error = normalizeCliError({ raw: true }, 'Fallback failed.'); + + expect(error.message).toBe('Fallback failed.'); + expect(error.details).toEqual(['Raw error: {"raw":true}']); + }); + + test('normalizes null and primitive thrown values', () => { + expect(normalizeCliError(undefined, 'Fallback failed.').details).toEqual([ + 'Raw error: ', + ]); + expect(normalizeCliError(null, 'Fallback failed.').details).toEqual([ + 'Raw error: ', + ]); + expect(normalizeCliError('plain failure', 'Fallback failed.').details).toEqual( + ['Raw error: plain failure'], + ); + }); + + test('normalizes circular thrown values through string fallback', () => { + const circular: Record = {}; + circular.self = circular; + const error = normalizeCliError(circular, 'Fallback failed.'); + + expect(error.details).toEqual(['Raw error: [object Object]']); + }); + + test('returns existing CliError instances unchanged', () => { + const original = createConfigError('Missing config.', ['Create pinme.toml']); + + expect(normalizeCliError(original, 'Fallback failed.')).toBe(original); + }); + + test('normalizes regular Error instances with deduped suggestions', () => { + const error = normalizeCliError(new Error('Boom'), 'Fallback failed.', [ + 'Retry', + 'Retry', + ]); + + expect(error.message).toBe('Boom'); + expect(error.suggestions).toEqual(['Retry']); + }); + + test('normalizes Error instances with empty messages to the fallback summary', () => { + const error = normalizeCliError(new Error(''), 'Fallback failed.'); + + expect(error.message).toBe('Fallback failed.'); + }); + + test('normalizes API-shaped thrown objects', () => { + const error = normalizeCliError( + { + config: { method: 'get', url: '/wallet' }, + response: { + data: { + data: { + error: 'Nested API failure', + }, + }, + }, + }, + 'Fallback failed.', + ); + + expect(error.message).toBe('Nested API failure'); + expect(error.stage).toBe('API request'); + }); + + test('includes non-Axios error code when response data is absent', () => { + const error = createApiError('API request', { + code: 'ECONNRESET', + message: 'socket hang up', + }); + + expect(error.details).toContain('Error code: ECONNRESET'); + expect(error.details).not.toContain('Reason: socket hang up'); + }); + + test('uses string response data as summary', () => { + const error = createApiError('API request', { + response: { + status: 502, + data: 'Bad gateway', + }, + }); + + expect(error.message).toBe('Bad gateway'); + expect(error.details).toContain('HTTP status: 502'); + }); + + test('includes nested API detail when it differs from the summary', () => { + const error = createApiError('API request', { + response: { + status: 400, + data: { + message: 'Top-level summary', + data: { + error: 'Nested detail', + }, + }, + }, + message: 'Request failed with status code 400', + }); + + expect(error.message).toBe('Top-level summary'); + expect(error.details).toContain('HTTP status: 400'); + expect(error.details).toContain('Error detail: Nested detail'); + expect(error.details).not.toContain( + 'Reason: Request failed with status code 400', + ); + }); + + test('falls back to the API stage when no response message exists', () => { + const error = createApiError('wallet lookup', {}); + + expect(error.message).toBe('wallet lookup failed.'); + expect(error.stage).toBe('wallet lookup'); + expect(error.details).toEqual([]); + }); + + test('includes raw reasons for non-generic API failures', () => { + const error = createApiError('API request', { + response: { + status: 502, + data: { error: 'Gateway wrapper failed' }, + }, + message: 'socket hang up', + }); + + expect(error.message).toBe('Gateway wrapper failed'); + expect(error.details).toContain('HTTP status: 502'); + expect(error.details).toContain('Reason: socket hang up'); + }); + + test('includes stringified business messages when they differ from summary', () => { + const error = createApiError('API request', { + response: { + data: { + code: 409, + msg: 123, + }, + }, + }); + + expect(error.message).toBe('123'); + expect(error.details).toContain('Business code: 409'); + expect(error.details).toContain('Business message: 123'); + }); + + test('printRechargeUrl writes to stdout by default', () => { + const messages: string[] = []; + const logSpy = vi.spyOn(console, 'log').mockImplementation((value = '') => { + messages.push(String(value)); + }); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + try { + printRechargeUrl('https://wallet.pinme.test'); + } finally { + logSpy.mockRestore(); + errorSpy.mockRestore(); + } + + expect(messages.join('\n')).toContain('Recharge URL:'); + expect(messages.join('\n')).toContain('https://wallet.pinme.test'); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + test('printCliError writes structured details and suggestions', () => { + const originalError = createConfigError('Missing config.', [ + 'Run pinme create app', + ]); + originalError.details = ['Project root: /tmp/app']; + const messages: string[] = []; + const spy = vi.spyOn(console, 'error').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + printCliError(originalError, 'Fallback failed.'); + } finally { + spy.mockRestore(); + } + + expect(messages.join('\n')).toContain('Error: Missing config.'); + expect(messages.join('\n')).toContain('Stage: configuration'); + expect(messages.join('\n')).toContain('Project root: /tmp/app'); + expect(messages.join('\n')).toContain('Run pinme create app'); + }); + + test('printCliError prints recharge URLs through the highlighted branch', () => { + const error = createConfigError('Needs funds.'); + error.details = ['Recharge URL: https://wallet.pinme.test']; + const messages: string[] = []; + const spy = vi.spyOn(console, 'error').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + printCliError(error, 'Fallback failed.'); + } finally { + spy.mockRestore(); + } + + expect(messages.join('\n')).toContain('Recharge URL:'); + expect(messages.join('\n')).toContain('https://wallet.pinme.test'); + }); + + test('printCliError skips next steps when suggestions are empty', () => { + const messages: string[] = []; + const spy = vi.spyOn(console, 'error').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + printCliError(createConfigError('No suggestions.'), 'Fallback failed.'); + } finally { + spy.mockRestore(); + } + + expect(messages.join('\n')).not.toContain('Next steps:'); + }); +}); diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts new file mode 100644 index 0000000..2cc7585 --- /dev/null +++ b/test/unit/config.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +const ENV_KEYS = [ + 'PINME_API_BASE', + 'IPFS_API_URL', + 'CAR_API_BASE', + 'PINME_WEB_URL', + 'MAX_RETRIES', + 'RETRY_DELAY_MS', + 'TIMEOUT_MS', + 'MAX_POLL_TIME_MINUTES', + 'POLL_INTERVAL_SECONDS', + 'POLL_TIMEOUT_SECONDS', +]; + +async function loadConfig(env: Record = {}) { + vi.resetModules(); + + for (const key of ENV_KEYS) { + delete process.env[key]; + } + Object.assign(process.env, env); + + return import('../../bin/utils/config'); +} + +describe('config', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test('trims trailing slashes from configured base URLs', async () => { + const { APP_CONFIG, getPinmeApiUrl, getIpfsApiUrl, getCarApiUrl } = + await loadConfig({ + PINME_API_BASE: 'https://api.pinme.test///', + IPFS_API_URL: 'https://ipfs.pinme.test/', + CAR_API_BASE: 'https://car.pinme.test/', + }); + + expect(APP_CONFIG.pinmeApiBase).toBe('https://api.pinme.test'); + expect(getPinmeApiUrl('root_domain')).toBe( + 'https://api.pinme.test/root_domain', + ); + expect(getIpfsApiUrl('/upload')).toBe('https://ipfs.pinme.test/upload'); + expect(getCarApiUrl('car/export')).toBe( + 'https://car.pinme.test/car/export', + ); + }); + + test('falls back when numeric environment values are invalid', async () => { + const { APP_CONFIG } = await loadConfig({ + MAX_RETRIES: 'not-a-number', + RETRY_DELAY_MS: '25', + MAX_POLL_TIME_MINUTES: '2', + POLL_INTERVAL_SECONDS: '3', + POLL_TIMEOUT_SECONDS: '4', + }); + + expect(APP_CONFIG.upload.maxRetries).toBe(2); + expect(APP_CONFIG.upload.retryDelayMs).toBe(25); + expect(APP_CONFIG.upload.maxPollTimeMs).toBe(120000); + expect(APP_CONFIG.upload.pollIntervalMs).toBe(3000); + expect(APP_CONFIG.upload.pollTimeoutMs).toBe(4000); + }); + + test('selects test wallet recharge URL for test-like API bases', async () => { + const { getWalletRechargeUrl } = await loadConfig({ + IPFS_API_URL: 'https://test-pinme.example/api', + }); + + expect(getWalletRechargeUrl()).toContain('test-pinme'); + }); + + test('selects production wallet recharge URL by default', async () => { + const { getWalletRechargeUrl } = await loadConfig({ + IPFS_API_URL: 'https://prod.example/api', + }); + + expect(getWalletRechargeUrl()).toContain('pinme.eth.limo'); + }); +}); diff --git a/test/unit/domainValidator.test.ts b/test/unit/domainValidator.test.ts new file mode 100644 index 0000000..c8c354c --- /dev/null +++ b/test/unit/domainValidator.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, test } from 'vitest'; +import fc from 'fast-check'; +import { + isDnsDomain, + normalizeDomain, + validateDnsDomain, +} from '../../bin/utils/domainValidator'; + +describe('domainValidator', () => { + test('normalizes protocol and a single trailing slash', () => { + expect(normalizeDomain('https://example.com/')).toBe('example.com'); + expect(normalizeDomain('http://demo.pinme/')).toBe('demo.pinme'); + expect(normalizeDomain('xhttps://example.com/')).toBe( + 'xhttps://example.com', + ); + expect(normalizeDomain('https://example.com/path/')).toBe( + 'example.com/path', + ); + }); + + test('detects DNS domains after normalization', () => { + expect(isDnsDomain('https://example.com/')).toBe(true); + expect(isDnsDomain('my-site')).toBe(false); + }); + + test('accepts complete DNS domains', () => { + expect(validateDnsDomain('example.com')).toEqual({ valid: true }); + expect(validateDnsDomain('sub.example.co')).toEqual({ valid: true }); + }); + + test('rejects incomplete and malformed DNS domains', () => { + expect(validateDnsDomain('localhost').valid).toBe(false); + expect(validateDnsDomain('example..com').message).toMatch(/Consecutive/); + expect(validateDnsDomain('-example.com').message).toMatch(/hyphens/); + expect(validateDnsDomain('example-.com').message).toMatch(/hyphens/); + expect(validateDnsDomain('exa_mple.com').message).toMatch( + /letters, numbers, and hyphens/, + ); + expect(validateDnsDomain('example.com.evil1').valid).toBe(false); + expect(validateDnsDomain('prefix example.com').valid).toBe(false); + expect(validateDnsDomain('example.com/path').valid).toBe(false); + expect(validateDnsDomain('example.com?x=1').valid).toBe(false); + }); + + test('rejects labels longer than 63 characters', () => { + const maxLabel = 'a'.repeat(63); + const longLabel = 'a'.repeat(64); + expect(validateDnsDomain(`${maxLabel}.com`).valid).toBe(true); + expect(validateDnsDomain(`${longLabel}.com`).message).toMatch( + /63 characters/, + ); + }); + + test('rejects empty labels in multiple positions', () => { + expect(validateDnsDomain('.example.com').message).toMatch(/Consecutive/); + expect(validateDnsDomain('example.com.').message).toMatch(/Consecutive/); + expect(validateDnsDomain('example...com').message).toMatch(/Consecutive/); + }); + + test('property: valid simple domains are accepted', () => { + fc.assert( + fc.property( + fc.array(fc.stringMatching(/^[a-z0-9]([a-z0-9-]{0,10}[a-z0-9])?$/), { + minLength: 1, + maxLength: 3, + }), + fc.stringMatching(/^[a-z]{2,10}$/), + (labels, tld) => { + fc.pre(labels.every((label) => label.length > 0)); + expect(validateDnsDomain([...labels, tld].join('.')).valid).toBe( + true, + ); + }, + ), + ); + }); +}); diff --git a/test/unit/history.test.ts b/test/unit/history.test.ts new file mode 100644 index 0000000..b55bf8b --- /dev/null +++ b/test/unit/history.test.ts @@ -0,0 +1,323 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'; +import path from 'path'; +import { tmpdir } from 'os'; +import { afterEach, describe, expect, test, vi } from 'vitest'; + +let tempHome: string | undefined; +let originalHome: string | undefined; +const trackEvent = vi.fn(); +const getRootDomain = vi.fn(async () => 'pinme.test'); + +async function loadHistory(fsMock?: Record) { + vi.resetModules(); + tempHome = mkdtempSync(path.join(tmpdir(), 'pinme-history-home-')); + originalHome = process.env.HOME; + process.env.HOME = tempHome; + if (fsMock) { + vi.doMock('fs-extra', () => ({ + ...fsMock, + default: fsMock, + })); + } + vi.doMock('os', () => ({ + homedir: () => tempHome, + default: { + homedir: () => tempHome, + }, + })); + vi.doMock('node:os', () => ({ + homedir: () => tempHome, + default: { + homedir: () => tempHome, + }, + })); + vi.doMock('../../bin/utils/tracker', () => ({ + default: { trackEvent }, + getTrackErrorReason: (error: unknown) => + error instanceof Error ? error.message : 'unknown_error', + })); + vi.doMock('../../bin/utils/pinmeApi', () => ({ + getRootDomain, + })); + return import('../../bin/utils/history'); +} + +describe('history', () => { + afterEach(() => { + vi.doUnmock('os'); + vi.doUnmock('node:os'); + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + originalHome = undefined; + vi.doUnmock('fs-extra'); + vi.doUnmock('../../bin/utils/tracker'); + vi.doUnmock('../../bin/utils/pinmeApi'); + vi.clearAllMocks(); + if (tempHome) { + rmSync(tempHome, { recursive: true, force: true }); + tempHome = undefined; + } + }); + + test('saves and reads upload history newest first', async () => { + const { saveUploadHistory, getUploadHistory } = await loadHistory(); + + expect( + saveUploadHistory({ + path: '/tmp/one', + contentHash: 'bafy-one', + previewHash: null, + size: 10, + fileCount: 1, + isDirectory: false, + }), + ).toBe(true); + expect( + saveUploadHistory({ + path: '/tmp/two', + filename: 'two', + contentHash: 'bafy-two', + previewHash: null, + size: 20, + fileCount: 2, + isDirectory: true, + pinmeUrl: 'demo', + }), + ).toBe(true); + + expect(getUploadHistory(1)).toMatchObject([ + { + filename: 'two', + contentHash: 'bafy-two', + fileCount: 2, + type: 'directory', + }, + ]); + }); + + test('displays preferred URLs and totals', async () => { + const { saveUploadHistory, displayUploadHistory } = await loadHistory(); + const messages: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + saveUploadHistory({ + path: '/tmp/site', + filename: 'site', + contentHash: 'bafy-site', + previewHash: null, + size: 2048, + fileCount: 3, + isDirectory: true, + pinmeUrl: 'demo', + }); + await displayUploadHistory(10); + } finally { + spy.mockRestore(); + } + + const output = messages.join('\n'); + expect(output).toContain('Upload History:'); + expect(output).toContain('1. site'); + expect(output).toContain('Path: /tmp/site'); + expect(output).toContain('IPFS CID: bafy-site'); + expect(output).toContain('https://demo.pinme.test'); + expect(output).toContain('Total Uploads: 1'); + expect(output).toContain('Total Files: 3'); + expect(output).toContain('Total Size: 2.00 KB'); + expect(output).toContain('Type: Directory'); + expect(trackEvent).toHaveBeenCalled(); + }); + + test('clearUploadHistory empties records', async () => { + const { saveUploadHistory, getUploadHistory, clearUploadHistory } = + await loadHistory(); + + saveUploadHistory({ + path: '/tmp/site', + contentHash: 'bafy-site', + previewHash: null, + size: 1, + }); + + expect(clearUploadHistory()).toBe(true); + expect(getUploadHistory()).toEqual([]); + }); + + test('displayUploadHistory handles missing root domain for bare URLs', async () => { + const { saveUploadHistory, displayUploadHistory } = await loadHistory(); + getRootDomain.mockRejectedValueOnce(new Error('root domain unavailable')); + const messages: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + saveUploadHistory({ + path: '/tmp/site', + filename: 'site', + contentHash: 'bafy-site', + previewHash: null, + size: 1, + shortUrl: 'short', + }); + await displayUploadHistory(10); + } finally { + spy.mockRestore(); + } + + expect(messages.join('\n')).toContain('https://short'); + }); + + test('displayUploadHistory reports an empty isolated history', async () => { + const { displayUploadHistory } = await loadHistory(); + const messages: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + await displayUploadHistory(5); + } finally { + spy.mockRestore(); + } + + expect(messages.join('\n')).toContain('No upload history found.'); + expect(trackEvent).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.objectContaining({ + record_count: 0, + limit: 5, + }), + ); + }); + + test('displayUploadHistory prefers DNS URLs over PinMe and short URLs', async () => { + const { saveUploadHistory, displayUploadHistory } = await loadHistory(); + const messages: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + saveUploadHistory({ + path: '/tmp/site', + filename: 'site', + contentHash: 'bafy-site', + previewHash: null, + size: 1024, + pinmeUrl: 'demo', + shortUrl: 'short', + dnsUrl: 'https://docs.example.com/', + }); + await displayUploadHistory(10); + } finally { + spy.mockRestore(); + } + + const output = messages.join('\n'); + expect(output).toContain('URL: https://docs.example.com'); + expect(output).not.toContain('https://demo.pinme.test'); + expect(output).not.toContain('https://short.pinme.test'); + }); + + test('getUploadHistory returns an empty list for malformed history files', async () => { + const { getUploadHistory } = await loadHistory(); + const messages: string[] = []; + const spy = vi.spyOn(console, 'error').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + mkdirSync(path.join(tempHome!, '.pinme'), { recursive: true }); + writeFileSync( + path.join(tempHome!, '.pinme', 'upload-history.json'), + '{not json', + ); + + try { + expect(getUploadHistory()).toEqual([]); + } finally { + spy.mockRestore(); + } + + expect(messages.join('\n')).toContain('Error reading upload history:'); + }); + + test('save and clear report false when history storage cannot be written', async () => { + const fsMock = { + existsSync: vi.fn(() => false), + mkdirSync: vi.fn(), + readJsonSync: vi.fn(() => ({ uploads: [] })), + writeJsonSync: vi.fn(() => { + throw new Error('disk denied'); + }), + }; + const { saveUploadHistory, clearUploadHistory } = await loadHistory(fsMock); + const messages: string[] = []; + const spy = vi.spyOn(console, 'error').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + expect( + saveUploadHistory({ + path: '/tmp/site', + contentHash: 'bafy-site', + previewHash: null, + size: 1, + }), + ).toBe(false); + expect(clearUploadHistory()).toBe(false); + } finally { + spy.mockRestore(); + } + + expect(fsMock.mkdirSync).toHaveBeenCalledWith(expect.any(String), { + recursive: true, + }); + expect(messages.join('\n')).toContain('Error saving upload history:'); + expect(messages.join('\n')).toContain('Error clearing upload history:'); + expect(trackEvent).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.objectContaining({ action: 'clear', reason: 'disk denied' }), + ); + }); + + test('formatHistoryUrl normalizes blank, absolute, dotted, and bare values', async () => { + const { formatHistoryUrl } = await loadHistory(); + + await expect(formatHistoryUrl()).resolves.toBeNull(); + await expect(formatHistoryUrl(' ')).resolves.toBeNull(); + await expect(formatHistoryUrl('https://example.com/path/')).resolves.toBe( + 'https://example.com/path', + ); + await expect(formatHistoryUrl('demo.example')).resolves.toBe( + 'https://demo.example', + ); + await expect( + formatHistoryUrl('demo', { + appendRootDomain: true, + rootDomain: 'pinme.test', + }), + ).resolves.toBe('https://demo.pinme.test'); + await expect( + formatHistoryUrl('http://demo/', { + appendRootDomain: true, + rootDomain: 'pinme.test', + }), + ).resolves.toBe('http://demo.pinme.test'); + await expect( + formatHistoryUrl('http://[bad', { + appendRootDomain: true, + rootDomain: 'pinme.test', + }), + ).resolves.toBe('http://[bad'); + }); +}); diff --git a/test/unit/uploadLimits.test.ts b/test/unit/uploadLimits.test.ts new file mode 100644 index 0000000..d20e885 --- /dev/null +++ b/test/unit/uploadLimits.test.ts @@ -0,0 +1,99 @@ +import { mkdirSync, rmSync, writeFileSync } from 'fs'; +import path from 'path'; +import { tmpdir } from 'os'; +import { mkdtempSync } from 'fs'; +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { + calculateDirectorySize, + checkDirectorySizeLimit, + checkFileSizeLimit, + formatSize, +} from '../../bin/utils/uploadLimits'; + +let tempDir: string | undefined; + +function makeTempDir(): string { + tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-upload-limits-')); + return tempDir; +} + +describe('uploadLimits', () => { + afterEach(() => { + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }); + tempDir = undefined; + } + }); + + test('checks file size against the default limit', () => { + const root = makeTempDir(); + const filePath = path.join(root, 'index.html'); + writeFileSync(filePath, Buffer.alloc(128)); + + expect(checkFileSizeLimit(filePath)).toMatchObject({ + size: 128, + exceeds: false, + }); + }); + + test('detects file and directory sizes above configured limits', async () => { + vi.resetModules(); + process.env.FILE_SIZE_LIMIT = '0'; + process.env.DIRECTORY_SIZE_LIMIT = '0'; + const limits = await import('../../bin/utils/uploadLimits'); + const root = makeTempDir(); + const filePath = path.join(root, 'index.html'); + writeFileSync(filePath, Buffer.alloc(1)); + + expect(limits.checkFileSizeLimit(filePath)).toMatchObject({ + size: 1, + limit: 0, + exceeds: true, + }); + expect(limits.checkDirectorySizeLimit(root)).toMatchObject({ + size: 1, + limit: 0, + exceeds: true, + }); + delete process.env.FILE_SIZE_LIMIT; + delete process.env.DIRECTORY_SIZE_LIMIT; + }); + + test('treats sizes equal to the configured limit as not exceeding', async () => { + vi.resetModules(); + process.env.FILE_SIZE_LIMIT = '1'; + process.env.DIRECTORY_SIZE_LIMIT = '1'; + const limits = await import('../../bin/utils/uploadLimits'); + const root = makeTempDir(); + const filePath = path.join(root, 'one-mb.bin'); + writeFileSync(filePath, Buffer.alloc(1024 * 1024)); + + expect(limits.checkFileSizeLimit(filePath).exceeds).toBe(false); + expect(limits.checkDirectorySizeLimit(root).exceeds).toBe(false); + delete process.env.FILE_SIZE_LIMIT; + delete process.env.DIRECTORY_SIZE_LIMIT; + }); + + test('calculates nested directory size', () => { + const root = makeTempDir(); + mkdirSync(path.join(root, 'assets'), { recursive: true }); + writeFileSync(path.join(root, 'index.html'), Buffer.alloc(10)); + writeFileSync(path.join(root, 'assets', 'app.js'), Buffer.alloc(15)); + + expect(calculateDirectorySize(root)).toBe(25); + expect(checkDirectorySizeLimit(root)).toMatchObject({ + size: 25, + exceeds: false, + }); + }); + + test('formats human readable sizes', () => { + expect(formatSize(12)).toBe('12 bytes'); + expect(formatSize(1024)).toBe('1.00 KB'); + expect(formatSize(2048)).toBe('2.00 KB'); + expect(formatSize(1024 * 1024)).toBe('1.00 MB'); + expect(formatSize(3 * 1024 * 1024)).toBe('3.00 MB'); + expect(formatSize(1024 * 1024 * 1024)).toBe('1.00 GB'); + expect(formatSize(2 * 1024 * 1024 * 1024)).toBe('2.00 GB'); + }); +}); diff --git a/test/unit/uploadService.test.ts b/test/unit/uploadService.test.ts new file mode 100644 index 0000000..10ff12a --- /dev/null +++ b/test/unit/uploadService.test.ts @@ -0,0 +1,340 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +const uploadToIpfsSplit = vi.fn(); +const getAuthConfig = vi.fn(); +const getRootDomain = vi.fn(async () => 'pinme.test'); +const getUid = vi.fn(() => 'device-uid'); + +vi.mock('../../bin/utils/uploadToIpfsSplit', () => ({ + default: uploadToIpfsSplit, +})); + +vi.mock('../../bin/utils/webLogin', () => ({ + getAuthConfig, +})); + +vi.mock('../../bin/utils/pinmeApi', () => ({ + getRootDomain, +})); + +vi.mock('../../bin/utils/getDeviceId', () => ({ + getUid, +})); + +async function loadService(env: Record = {}) { + vi.resetModules(); + process.env.IPFS_PREVIEW_URL = + env.IPFS_PREVIEW_URL || 'https://preview.pinme.test/#/preview/'; + process.env.PROJECT_PREVIEW_URL = + env.PROJECT_PREVIEW_URL || 'https://project.pinme.test/'; + if ('SECRET_KEY' in env) { + if (env.SECRET_KEY === undefined) { + delete process.env.SECRET_KEY; + } else { + process.env.SECRET_KEY = env.SECRET_KEY; + } + } else { + delete process.env.SECRET_KEY; + } + return import('../../bin/services/uploadService'); +} + +describe('uploadService', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.doUnmock('crypto-js'); + getRootDomain.mockResolvedValue('pinme.test'); + getUid.mockReturnValue('device-uid'); + }); + + test('prefers DNS URL over PinMe, short, and management URLs', async () => { + const { resolveUploadUrls } = await loadService(); + + const result = await resolveUploadUrls( + 'bafybeicid', + { + dnsUrl: 'example.com/', + pinmeUrl: 'my-site', + shortUrl: 'short', + }, + undefined, + 'uid-1', + ); + + expect(result).toEqual({ + publicUrl: 'https://example.com', + managementUrl: 'https://preview.pinme.test/#/preview/bafybeicid', + }); + }); + + test('appends root domain for bare PinMe subdomains', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls('bafybeicid', { pinmeUrl: 'demo' }, undefined, 'uid-1'), + ).resolves.toMatchObject({ + publicUrl: 'https://demo.pinme.test', + }); + }); + + test('keeps absolute and dotted short URLs without root-domain lookup', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { shortUrl: 'https://already.example/path/' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'https://already.example/path/', + }); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { shortUrl: 'http://already.example/path/' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'http://already.example/path/', + }); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { shortUrl: 'short.example' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'https://short.example', + }); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { shortUrl: 'xhttp://short.example/' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'https://xhttp://short.example/', + }); + }); + + test('accepts http PinMe URLs and preserves explicit protocol', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { pinmeUrl: 'http://demo.pinme.test/path/' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'http://demo.pinme.test/path', + }); + }); + + test('falls back to protocol-prefixed text for invalid preferred URLs', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { dnsUrl: 'bad host/' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'https://bad host', + }); + }); + + test('does not treat embedded protocol text as an absolute preferred URL', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { dnsUrl: 'xhttp://example.com/' }, + undefined, + 'uid-1', + ), + ).resolves.toMatchObject({ + publicUrl: 'https://xhttp//example.com', + }); + }); + + test('falls back to bare subdomain when root domain lookup fails', async () => { + const { resolveUploadUrls } = await loadService(); + getRootDomain.mockRejectedValueOnce(new Error('root domain failed')); + + await expect( + resolveUploadUrls('bafybeicid', { pinmeUrl: 'demo' }, undefined, 'uid-1'), + ).resolves.toMatchObject({ + publicUrl: 'https://demo', + }); + }); + + test('ignores blank preferred URLs and falls back to management URL', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls( + 'bafybeicid', + { dnsUrl: ' ', pinmeUrl: '', shortUrl: ' ' }, + undefined, + 'uid-1', + ), + ).resolves.toEqual({ + publicUrl: 'https://preview.pinme.test/#/preview/bafybeicid', + managementUrl: 'https://preview.pinme.test/#/preview/bafybeicid', + }); + }); + + test('falls back to project management URL when project name is present', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls('bafybeicid', undefined, 'demo-project', 'uid-1'), + ).resolves.toEqual({ + publicUrl: 'https://project.pinme.test/demo-project', + managementUrl: 'https://project.pinme.test/demo-project', + }); + }); + + test('trims project names and falls back to device uid when uid is blank', async () => { + const { resolveUploadUrls } = await loadService(); + + await expect( + resolveUploadUrls('bafybeicid', undefined, ' demo-project ', ' '), + ).resolves.toEqual({ + publicUrl: 'https://project.pinme.test/demo-project', + managementUrl: 'https://project.pinme.test/demo-project', + }); + expect(getUid).toHaveBeenCalled(); + }); + + test('uses secretKey to hide raw CID in preview management URLs', async () => { + const { resolveUploadUrls } = await loadService({ SECRET_KEY: 'secret' }); + + const result = await resolveUploadUrls( + 'bafybeicid', + undefined, + undefined, + 'uid-1', + ); + + expect(result.managementUrl).toMatch( + /^https:\/\/preview\.pinme\.test\/#\/preview\/.+/, + ); + expect(result.managementUrl).not.toContain('bafybeicid'); + expect(result.publicUrl).toBe(result.managementUrl); + }); + + test('secretKey encryption produces URL-safe CID tokens that include uid input', async () => { + const { resolveUploadUrls } = await loadService({ SECRET_KEY: 'secret' }); + + const first = await resolveUploadUrls( + 'bafybeicid', + undefined, + undefined, + 'uid-1', + ); + const second = await resolveUploadUrls( + 'bafybeicid', + undefined, + undefined, + 'uid-2', + ); + const firstToken = first.managementUrl.split('/preview/').at(-1)!; + const secondToken = second.managementUrl.split('/preview/').at(-1)!; + + expect(firstToken).not.toBe(secondToken); + expect(firstToken).not.toContain('bafybeicid'); + expect(firstToken).not.toMatch(/[+/=]/); + expect(secondToken).not.toMatch(/[+/=]/); + }); + + test('secretKey encryption sanitizes plus slash and padding deterministically', async () => { + const encrypt = vi.fn((message: string) => ({ + toString: () => `+/${message}==`, + })); + vi.doMock('crypto-js', () => ({ + default: { + RC4: { encrypt }, + }, + })); + const { resolveUploadUrls } = await loadService({ SECRET_KEY: 'secret' }); + + const result = await resolveUploadUrls( + 'bafybeicid', + undefined, + undefined, + 'uid-1', + ); + + expect(encrypt).toHaveBeenCalledWith('bafybeicid-uid-1', 'secret'); + expect(result.managementUrl).toBe( + 'https://preview.pinme.test/#/preview/-_bafybeicid-uid-1', + ); + expect(result.managementUrl.split('/preview/').at(-1)).not.toMatch( + /[+/=]/, + ); + }); + + + test('uploadPath rejects when auth config is absent', async () => { + const { uploadPath } = await loadService(); + getAuthConfig.mockReturnValue(null); + + await expect(uploadPath('/tmp/site')).rejects.toThrow(/Please login first/); + expect(uploadToIpfsSplit).not.toHaveBeenCalled(); + }); + + test('uploadPath returns normalized upload result URLs', async () => { + const { uploadPath } = await loadService(); + getAuthConfig.mockReturnValue({ + address: '0xabc', + token: 'token', + }); + uploadToIpfsSplit.mockResolvedValue({ + contentHash: 'bafybeicid', + shortUrl: 'short', + }); + + await expect(uploadPath('/tmp/site', { action: 'upload' })).resolves.toEqual({ + contentHash: 'bafybeicid', + shortUrl: 'short', + pinmeUrl: undefined, + dnsUrl: undefined, + publicUrl: 'https://short.pinme.test', + managementUrl: 'https://preview.pinme.test/#/preview/bafybeicid', + }); + expect(uploadToIpfsSplit).toHaveBeenCalledWith('/tmp/site', { + action: 'upload', + importAsCar: undefined, + projectName: undefined, + uid: '0xabc', + }); + }); + + test('uploadPath rejects upload responses without a content hash', async () => { + const { uploadPath } = await loadService(); + getAuthConfig.mockReturnValue({ + address: '0xabc', + token: 'token', + }); + uploadToIpfsSplit.mockResolvedValue({}); + + await expect(uploadPath('/tmp/site')).rejects.toThrow(/no content hash/); + + uploadToIpfsSplit.mockResolvedValueOnce(null); + await expect(uploadPath('/tmp/site')).rejects.toThrow(/no content hash/); + }); +}); diff --git a/test/unit/webLogin.test.ts b/test/unit/webLogin.test.ts new file mode 100644 index 0000000..bae1c39 --- /dev/null +++ b/test/unit/webLogin.test.ts @@ -0,0 +1,153 @@ +import fs from 'fs-extra'; +import { mkdtempSync, rmSync } from 'fs'; +import path from 'path'; +import { tmpdir } from 'os'; +import { afterEach, describe, expect, test, vi } from 'vitest'; + +let tempHome: string | undefined; +let originalHome: string | undefined; + +async function loadWebLogin() { + vi.resetModules(); + tempHome = mkdtempSync(path.join(tmpdir(), 'pinme-web-login-home-')); + originalHome = process.env.HOME; + process.env.HOME = tempHome; + vi.doMock('os', () => ({ + homedir: () => tempHome, + default: { + homedir: () => tempHome, + }, + })); + vi.doMock('node:os', () => ({ + homedir: () => tempHome, + default: { + homedir: () => tempHome, + }, + })); + return import('../../bin/utils/webLogin'); +} + +describe('webLogin', () => { + afterEach(() => { + vi.doUnmock('os'); + vi.doUnmock('node:os'); + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + originalHome = undefined; + if (tempHome) { + rmSync(tempHome, { recursive: true, force: true }); + tempHome = undefined; + } + }); + + test('stores and reads auth tokens with auth headers', async () => { + const { setAuthToken, getAuthConfig, getAuthHeaders } = await loadWebLogin(); + + expect(setAuthToken('0xabc-jwt-token')).toEqual({ + address: '0xabc', + token: 'jwt-token', + }); + expect(getAuthConfig()).toEqual({ + address: '0xabc', + token: 'jwt-token', + }); + expect(getAuthHeaders()).toEqual({ + 'token-address': '0xabc', + 'authentication-tokens': 'jwt-token', + }); + + expect(getAuthConfig()).toMatchObject({ address: '0xabc' }); + }); + + test('trims address and token content before storing auth', async () => { + const { setAuthToken, getAuthConfig, getAuthHeaders } = await loadWebLogin(); + + expect(setAuthToken(' 0xabc - jwt-token ')).toEqual({ + address: '0xabc', + token: 'jwt-token', + }); + expect(getAuthConfig()).toEqual({ + address: '0xabc', + token: 'jwt-token', + }); + expect(getAuthHeaders()).toEqual({ + 'token-address': '0xabc', + 'authentication-tokens': 'jwt-token', + }); + }); + + test('rejects malformed combined auth tokens', async () => { + const { setAuthToken } = await loadWebLogin(); + + expect(() => setAuthToken('-jwt-token')).toThrow( + /Address or token is empty|Invalid token/, + ); + expect(() => setAuthToken('0xabc-')).toThrow(/Invalid token format/); + expect(() => setAuthToken('missingdash')).toThrow(/Invalid token format/); + expect(() => setAuthToken(' -jwt-token')).toThrow( + 'Invalid token content. Address or token is empty.', + ); + expect(() => setAuthToken('0xabc- ')).toThrow( + 'Invalid token content. Address or token is empty.', + ); + }); + + test('clears auth tokens and makes headers unavailable', async () => { + const { setAuthToken, clearAuthToken, getAuthConfig, getAuthHeaders } = + await loadWebLogin(); + + setAuthToken('0xabc-jwt-token'); + clearAuthToken(); + + expect(getAuthConfig()).toBeNull(); + expect(() => getAuthHeaders()).toThrow('Auth not set. Run: pinme login'); + }); + + test('getAuthConfig returns null for malformed or incomplete auth files', async () => { + const { getAuthConfig } = await loadWebLogin(); + const authDir = path.join(tempHome!, '.pinme'); + const authFile = path.join(authDir, 'auth.json'); + fs.ensureDirSync(authDir); + + fs.writeJsonSync(authFile, { address: '0xabc' }); + expect(getAuthConfig()).toBeNull(); + + fs.writeFileSync(authFile, '{bad'); + expect(getAuthConfig()).toBeNull(); + }); + + test('login delegates to the singleton web login manager', async () => { + const { login, webLoginManager } = await loadWebLogin(); + const authConfig = { address: '0xabc', token: 'jwt-token' }; + const spy = vi + .spyOn(webLoginManager, 'login') + .mockResolvedValue(authConfig); + + try { + await expect(login()).resolves.toBe(authConfig); + } finally { + spy.mockRestore(); + } + }); + + test('logout clears auth and reports success', async () => { + const { setAuthToken, logout, getAuthConfig } = await loadWebLogin(); + const messages: string[] = []; + const spy = vi.spyOn(console, 'log').mockImplementation((value = '') => { + messages.push(String(value)); + }); + + try { + setAuthToken('0xabc-jwt-token'); + await logout(); + } finally { + spy.mockRestore(); + } + + expect(getAuthConfig()).toBeNull(); + expect(messages.join('\n')).toContain('Logged out successfully'); + }); +}); diff --git a/tests/background-install-status.test.mjs b/tests/background-install-status.test.mjs new file mode 100644 index 0000000..ce463ce --- /dev/null +++ b/tests/background-install-status.test.mjs @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict'; +import { + mkdirSync, + mkdtempSync, + existsSync, + rmSync, + utimesSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; +import { test } from 'vitest'; + +async function loadHelper() { + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-install-helper-')); + const outfile = path.join(tempDir, 'installProjectDependencies.cjs'); + + await build({ + entryPoints: [path.resolve('bin/utils/installProjectDependencies.ts')], + outfile, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node18', + }); + + return import(pathToFileURL(outfile).href); +} + +function makeProject() { + return mkdtempSync(path.join(tmpdir(), 'pinme-install-project-')); +} + +test('readBackgroundInstallStatus treats fresh log-only markers as running', async () => { + const { + INSTALL_LOG_FILE, + readBackgroundInstallStatus, + } = await loadHelper(); + const projectDir = makeProject(); + + try { + writeFileSync(path.join(projectDir, INSTALL_LOG_FILE), 'starting npm ci\n'); + + assert.deepEqual(readBackgroundInstallStatus(projectDir), { + status: 'running', + exitCode: null, + logPath: path.join(projectDir, INSTALL_LOG_FILE), + }); + } finally { + rmSync(projectDir, { recursive: true, force: true }); + } +}); + +test('readBackgroundInstallStatus detects stale log-only markers as interrupted', async () => { + const { + INSTALL_LOG_FILE, + readBackgroundInstallStatus, + } = await loadHelper(); + const projectDir = makeProject(); + const logPath = path.join(projectDir, INSTALL_LOG_FILE); + + try { + mkdirSync(projectDir, { recursive: true }); + writeFileSync(logPath, 'starting npm ci\n'); + const staleTime = new Date(Date.now() - 2 * 60 * 1000); + utimesSync(logPath, staleTime, staleTime); + + assert.deepEqual(readBackgroundInstallStatus(projectDir), { + status: 'interrupted', + exitCode: null, + logPath, + }); + } finally { + rmSync(projectDir, { recursive: true, force: true }); + } +}); + +test('readBackgroundInstallStatus detects dead background install pid as interrupted', async () => { + const { + INSTALL_LOG_FILE, + INSTALL_PID_FILE, + readBackgroundInstallStatus, + } = await loadHelper(); + const projectDir = makeProject(); + const logPath = path.join(projectDir, INSTALL_LOG_FILE); + + try { + writeFileSync(logPath, 'starting npm ci\n'); + writeFileSync(path.join(projectDir, INSTALL_PID_FILE), '999999999'); + + assert.deepEqual(readBackgroundInstallStatus(projectDir), { + status: 'interrupted', + exitCode: null, + logPath, + }); + } finally { + rmSync(projectDir, { recursive: true, force: true }); + } +}); + +test('stopBackgroundInstall clears a dead background install pid marker', async () => { + const { + INSTALL_LOG_FILE, + INSTALL_PID_FILE, + stopBackgroundInstall, + } = await loadHelper(); + const projectDir = makeProject(); + const logPath = path.join(projectDir, INSTALL_LOG_FILE); + const pidPath = path.join(projectDir, INSTALL_PID_FILE); + + try { + writeFileSync(logPath, 'starting npm ci\n'); + writeFileSync(pidPath, '999999999'); + + assert.equal(await stopBackgroundInstall(projectDir), false); + assert.equal(readFileExists(pidPath), false); + } finally { + rmSync(projectDir, { recursive: true, force: true }); + } +}); + +test('buildBackgroundInstallCommand captures Windows npm exit code after command finishes', async () => { + const { + buildBackgroundInstallCommand, + } = await loadHelper(); + + const command = buildBackgroundInstallCommand( + 'ci', + 'C:\\Users\\Pin Me\\project\\.pinme-install.log', + 'C:\\Users\\Pin Me\\project\\.pinme-install.exitcode', + 'win32', + ); + + assert.deepEqual(command.shellArgs.slice(0, 4), ['/d', '/s', '/v:on', '/c']); + assert.match(command.shellArgs[4], /echo !errorlevel! >/); + assert.doesNotMatch(command.shellArgs[4], /%errorlevel%/); + assert.match(command.shellArgs[4], /"C:\\Users\\Pin Me\\project\\.pinme-install.log"/); +}); + +function readFileExists(filePath) { + return existsSync(filePath); +} diff --git a/tests/download-file.test.mjs b/tests/download-file.test.mjs new file mode 100644 index 0000000..d428b25 --- /dev/null +++ b/tests/download-file.test.mjs @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; +import { test } from 'vitest'; + +async function loadHelper() { + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-download-helper-')); + const outfile = path.join(tempDir, 'downloadFile.cjs'); + + await build({ + entryPoints: [path.resolve('bin/utils/downloadFile.ts')], + outfile, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node18', + }); + + const helper = await import(pathToFileURL(outfile).href); + return { + helper, + cleanup: () => rmSync(tempDir, { recursive: true, force: true }), + }; +} + +test('downloadFileWithRetries downloads through Node HTTP without curl', async () => { + const { helper, cleanup } = await loadHelper(); + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-download-target-')); + const destination = path.join(tempDir, 'template.zip'); + const body = Buffer.alloc(256, 'a'); + + try { + const result = await helper.downloadFileWithRetries('https://example.test/template.zip', destination, { + attempts: 3, + retryDelayMs: 1, + minBytes: 100, + request: async () => ({ data: Readable.from([body]) }), + }); + + assert.equal(result.attempts, 1); + assert.equal(result.bytes, body.length); + assert.deepEqual(readFileSync(destination), body); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + cleanup(); + } +}); + +test('downloadFileWithRetries retries HTTP failures', async () => { + const { helper, cleanup } = await loadHelper(); + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-download-retry-')); + const destination = path.join(tempDir, 'template.zip'); + const body = Buffer.alloc(256, 'b'); + let requests = 0; + + try { + const failures = []; + const result = await helper.downloadFileWithRetries('https://example.test/template.zip', destination, { + attempts: 3, + retryDelayMs: 1, + minBytes: 100, + request: async () => { + requests += 1; + + if (requests < 3) { + const error = new Error('Request failed with status code 503'); + error.response = { + status: 503, + statusText: 'Service Unavailable', + }; + throw error; + } + + return { data: Readable.from([body]) }; + }, + onAttemptFailure: (attempt, error) => { + failures.push({ attempt, message: helper.getDownloadErrorMessage(error) }); + }, + }); + + assert.equal(result.attempts, 3); + assert.equal(requests, 3); + assert.deepEqual(failures, [ + { attempt: 1, message: 'HTTP 503 Service Unavailable' }, + { attempt: 2, message: 'HTTP 503 Service Unavailable' }, + ]); + assert.deepEqual(readFileSync(destination), body); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + cleanup(); + } +}); + +test('downloadFileWithRetries rejects tiny downloads and removes partial files', async () => { + const { helper, cleanup } = await loadHelper(); + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-download-small-')); + const destination = path.join(tempDir, 'template.zip'); + + try { + await assert.rejects( + () => helper.downloadFileWithRetries('https://example.test/template.zip', destination, { + attempts: 2, + retryDelayMs: 1, + minBytes: 100, + request: async () => ({ data: Readable.from(['tiny']) }), + }), + /Downloaded file is too small/, + ); + + assert.throws(() => readFileSync(destination), /ENOENT/); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + cleanup(); + } +}); diff --git a/tests/prebuilt-dist-config.test.mjs b/tests/prebuilt-dist-config.test.mjs new file mode 100644 index 0000000..6b05b2f --- /dev/null +++ b/tests/prebuilt-dist-config.test.mjs @@ -0,0 +1,148 @@ +import assert from 'node:assert/strict'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; +import { test } from 'vitest'; + +async function loadHelper() { + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-prebuilt-helper-')); + const outfile = path.join(tempDir, 'prebuiltDistConfig.cjs'); + + await build({ + entryPoints: [path.resolve('bin/utils/prebuiltDistConfig.ts')], + outfile, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node18', + }); + + return import(pathToFileURL(outfile).href); +} + +function makeDist(files) { + const distDir = mkdtempSync(path.join(tmpdir(), 'pinme-dist-')); + + for (const [relativePath, content] of Object.entries(files)) { + const filePath = path.join(distDir, relativePath); + mkdirSync(path.dirname(filePath), { recursive: true }); + writeFileSync(filePath, content); + } + + return distDir; +} + +test('patchPrebuiltFrontendDist replaces API and auth placeholders', async () => { + const { patchPrebuiltFrontendDist } = await loadHelper(); + const distDir = makeDist({ + 'index.html': '', + 'assets/index.js': [ + 'const api="__PINME_VITE_API_URL__";', + 'const key="__PINME_AUTH_API_KEY__";', + 'const domain="__PINME_AUTH_DOMAIN__";', + 'const project="__PINME_AUTH_PROJECT_ID__";', + 'const tenant="__PINME_TENANT_ID__";', + ].join('\n'), + }); + + try { + const result = patchPrebuiltFrontendDist(distDir, { + api_domain: 'https://demo.pinme.pro', + public_client_config: { + auth_api_key: 'firebase-key', + auth_domain: 'demo.firebaseapp.com', + auth_project_id: 'firebase-project', + tenant_id: 'tenant-123', + }, + }); + + const bundle = readFileSync(path.join(distDir, 'assets/index.js'), 'utf8'); + assert.equal(result.apiUrlReplacements, 1); + assert.equal(result.authReplacements, 4); + assert.match(bundle, /https:\/\/demo\.pinme\.pro/); + assert.match(bundle, /firebase-key/); + assert.match(bundle, /demo\.firebaseapp\.com/); + assert.match(bundle, /firebase-project/); + assert.match(bundle, /tenant-123/); + assert.doesNotMatch(bundle, /__PINME_/); + } finally { + rmSync(distDir, { recursive: true, force: true }); + } +}); + +test('patchPrebuiltFrontendDist clears auth placeholders when auth config is absent', async () => { + const { patchPrebuiltFrontendDist } = await loadHelper(); + const distDir = makeDist({ + 'assets/index.js': [ + 'const api="__PINME_VITE_API_URL__";', + 'const key="__PINME_AUTH_API_KEY__";', + 'const domain="__PINME_AUTH_DOMAIN__";', + ].join('\n'), + }); + + try { + const result = patchPrebuiltFrontendDist(distDir, { + api_domain: 'https://demo.pinme.pro', + }); + + const bundle = readFileSync(path.join(distDir, 'assets/index.js'), 'utf8'); + assert.equal(result.apiUrlReplacements, 1); + assert.equal(result.authReplacements, 2); + assert.equal( + bundle, + [ + 'const api="https://demo.pinme.pro";', + 'const key="";', + 'const domain="";', + ].join('\n'), + ); + } finally { + rmSync(distDir, { recursive: true, force: true }); + } +}); + +test('patchPrebuiltFrontendDist fails when API placeholder is missing', async () => { + const { patchPrebuiltFrontendDist } = await loadHelper(); + const distDir = makeDist({ + 'assets/index.js': 'const key="__PINME_AUTH_API_KEY__";', + }); + + try { + assert.throws( + () => patchPrebuiltFrontendDist(distDir, { + api_domain: 'https://demo.pinme.pro', + }), + /missing required Pinme config placeholder/, + ); + } finally { + rmSync(distDir, { recursive: true, force: true }); + } +}); + +test('patchPrebuiltFrontendDist skips unsupported files', async () => { + const { patchPrebuiltFrontendDist } = await loadHelper(); + const distDir = makeDist({ + 'assets/index.js': 'const api="__PINME_VITE_API_URL__";', + 'assets/logo.svg': '__PINME_VITE_API_URL__', + }); + + try { + patchPrebuiltFrontendDist(distDir, { + api_domain: 'https://demo.pinme.pro', + }); + assert.equal( + readFileSync(path.join(distDir, 'assets/logo.svg'), 'utf8'), + '__PINME_VITE_API_URL__', + ); + } finally { + rmSync(distDir, { recursive: true, force: true }); + } +}); diff --git a/tests/tracker-error-reason.test.mjs b/tests/tracker-error-reason.test.mjs new file mode 100644 index 0000000..9c8dccf --- /dev/null +++ b/tests/tracker-error-reason.test.mjs @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; +import { test } from 'vitest'; + +async function loadHelper() { + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-tracker-helper-')); + const outfile = path.join(tempDir, 'tracker.cjs'); + + await build({ + entryPoints: [path.resolve('bin/utils/tracker.ts')], + outfile, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node18', + }); + + const helper = await import(pathToFileURL(outfile).href); + return { + helper, + cleanup: () => rmSync(tempDir, { recursive: true, force: true }), + }; +} + +test('getTrackErrorReason normalizes HTML API responses', async () => { + const { helper, cleanup } = await loadHelper(); + + try { + assert.equal( + helper.getTrackErrorReason({ + response: { + status: 502, + data: 'Bad Gateway', + }, + message: 'Request failed with status code 502', + }), + 'api_returned_html', + ); + } finally { + cleanup(); + } +}); + +test('getTrackErrorReason normalizes gateway status failures', async () => { + const { helper, cleanup } = await loadHelper(); + + try { + assert.equal( + helper.getTrackErrorReason(new Error('Request failed with status code 520')), + 'gateway_520', + ); + } finally { + cleanup(); + } +}); + +test('getTrackErrorReason prefers nested cause over command wrapper messages', async () => { + const { helper, cleanup } = await loadHelper(); + + try { + const htmlError = { + response: { + status: 520, + data: 'edge error', + }, + message: 'Request failed with status code 520', + }; + const wrappedError = new Error('frontend deploy failed.'); + wrappedError.cause = htmlError; + + assert.equal( + helper.getTrackErrorReason(wrappedError), + 'api_returned_html', + ); + } finally { + cleanup(); + } +}); + +test('getTrackErrorReason normalizes authentication failures', async () => { + const { helper, cleanup } = await loadHelper(); + + try { + assert.equal( + helper.getTrackErrorReason(new Error('Token authentication failed')), + 'token_auth_failed', + ); + } finally { + cleanup(); + } +}); diff --git a/tests/worker-metadata.test.mjs b/tests/worker-metadata.test.mjs new file mode 100644 index 0000000..d15165c --- /dev/null +++ b/tests/worker-metadata.test.mjs @@ -0,0 +1,127 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; +import { test } from 'vitest'; + +async function loadHelper() { + const tempDir = mkdtempSync(path.join(tmpdir(), 'pinme-worker-metadata-')); + const outfile = path.join(tempDir, 'workerMetadata.cjs'); + + await build({ + entryPoints: [path.resolve('bin/utils/workerMetadata.ts')], + outfile, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node18', + }); + + return import(pathToFileURL(outfile).href); +} + +function validMetadata(projectName = 'demo-project') { + return JSON.stringify({ + main_module: 'worker.js', + project_name: projectName, + bindings: [ + { + type: 'secret_text', + name: 'API_KEY', + text: 'real-api-key', + }, + { + type: 'plain_text', + name: 'PROJECT_NAME', + text: projectName, + }, + ], + compatibility_date: '2024-01-01', + }); +} + +test('validateWorkerMetadataForCreate accepts platform metadata', async () => { + const { validateWorkerMetadataForCreate } = await loadHelper(); + + assert.doesNotThrow(() => { + validateWorkerMetadataForCreate(validMetadata(), 'demo-project'); + }); +}); + +test('validateWorkerMetadataForCreate rejects template placeholder metadata', async () => { + const { validateWorkerMetadataForCreate } = await loadHelper(); + const templateMetadata = JSON.stringify({ + api_key: 'xxx', + main_module: 'worker.js', + project_name: 'project_name', + compatibility_date: '2024-01-01', + bindings: [ + { + type: 'secret_text', + name: 'API_KEY', + text: 'xxx', + }, + ], + }); + + assert.throws( + () => validateWorkerMetadataForCreate(templateMetadata, 'demo-project'), + /real API_KEY binding/, + ); +}); + +test('validateWorkerMetadataForCreate rejects flagged API_KEY placeholder', async () => { + const { validateWorkerMetadataForCreate } = await loadHelper(); + const metadata = JSON.stringify({ + main_module: 'worker.js', + project_name: 'demo-project', + bindings: [ + { + type: 'secret_text', + name: 'API_KEY', + text: '__PINME_API_KEY__', + }, + { + type: 'plain_text', + name: 'PROJECT_NAME', + text: 'demo-project', + }, + ], + }); + + assert.throws( + () => validateWorkerMetadataForCreate(metadata, 'demo-project'), + /real API_KEY binding/, + ); +}); + +test('validateWorkerMetadataForCreate requires matching PROJECT_NAME binding', async () => { + const { validateWorkerMetadataForCreate } = await loadHelper(); + const metadata = JSON.stringify({ + main_module: 'worker.js', + project_name: 'demo-project', + bindings: [ + { + type: 'secret_text', + name: 'API_KEY', + text: 'real-api-key', + }, + ], + }); + + assert.throws( + () => validateWorkerMetadataForCreate(metadata, 'demo-project'), + /PROJECT_NAME binding/, + ); +}); + +test('validateWorkerMetadataForCreate rejects invalid JSON', async () => { + const { validateWorkerMetadataForCreate } = await loadHelper(); + + assert.throws( + () => validateWorkerMetadataForCreate('{not json', 'demo-project'), + /valid JSON/, + ); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..09aec12 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": false, + "noEmit": true, + "types": ["node", "vitest/globals"] + }, + "include": [ + "bin/**/*.ts", + "test/**/*.ts", + "vitest.config.ts", + "vitest.mutation.config.ts" + ], + "exclude": ["node_modules", "dist", "coverage", "reports"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0ed2455 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: [ + 'test/**/*.{test,spec}.ts', + 'test/**/*.{test,spec}.mjs', + 'tests/**/*.{test,spec}.mjs', + ], + exclude: ['node_modules', 'dist', 'coverage', 'reports', '.stryker-tmp'], + setupFiles: ['test/setup/nock.ts'], + testTimeout: 30000, + hookTimeout: 30000, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + // Keep the coverage gate on in-process core modules. Command files are + // exercised by test:cli against the bundled CLI; that subprocess coverage + // is not reliably attributed back to TypeScript source files. + include: [ + 'bin/utils/domainValidator.ts', + 'bin/utils/config.ts', + 'bin/utils/uploadLimits.ts', + 'bin/utils/apiClient.ts', + 'bin/utils/cliError.ts', + 'bin/utils/history.ts', + 'bin/utils/pinmeApi.ts', + 'bin/utils/webLogin.ts', + 'bin/services/uploadService.ts', + ], + exclude: [ + // Login callback/browser UI and CLI entrypoints are covered by CLI tests. + 'bin/index.ts', + 'bin/login.ts', + ], + statements: 85, + branches: 80, + functions: 85, + lines: 85, + }, + }, +}); diff --git a/vitest.mutation.config.ts b/vitest.mutation.config.ts new file mode 100644 index 0000000..1aaaca0 --- /dev/null +++ b/vitest.mutation.config.ts @@ -0,0 +1,23 @@ +import { mergeConfig } from 'vitest/config'; + +import baseConfig from './vitest.config'; + +export default mergeConfig(baseConfig, { + test: { + include: [ + 'test/unit/**/*.{test,spec}.ts', + 'test/integration/**/*.{test,spec}.ts', + 'test/login-tracking-source.test.mjs', + 'tests/**/*.{test,spec}.mjs', + ], + exclude: [ + 'node_modules', + 'dist', + 'coverage', + 'reports', + '.stryker-tmp', + 'test/cli/**', + 'test/pack/**', + ], + }, +});