diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fe1fbce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Test (Node ${{ matrix.node-version }}, React ${{ matrix.react }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22, 24] + react: [18, 19] + defaults: + run: + working-directory: shieldlabs-react + steps: + - uses: actions/checkout@v5 + with: + path: shieldlabs-react + persist-credentials: false + # @shieldlabs-ai/js is not on npm yet, so it is built from its repository next to this one. + - uses: actions/checkout@v5 + with: + repository: ShieldLabs-ai/shieldlabs-js + ref: main + path: shieldlabs-js + persist-credentials: false + - uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: | + shieldlabs-react/package-lock.json + shieldlabs-js/package-lock.json + - name: Build and pack @shieldlabs-ai/js + working-directory: shieldlabs-js + run: | + npm ci + npm run build + npm pack + - run: npm ci + - name: Install the packed @shieldlabs-ai/js and React ${{ matrix.react }} + env: + REACT_MAJOR: ${{ matrix.react }} + run: | + set -- ../shieldlabs-js/shieldlabs-ai-js-*.tgz + if [ "$REACT_MAJOR" = "18" ]; then + set -- "$@" react@18 react-dom@18 @types/react@18 @types/react-dom@18 + fi + npm install --no-save "$@" + node -p "'React ' + require('react/package.json').version" + - run: npm run typecheck + - run: npm run lint + - run: npm test -- --coverage + - run: npm run build + + example-vite: + name: Vite example + runs-on: ubuntu-latest + defaults: + run: + working-directory: shieldlabs-react + steps: + - uses: actions/checkout@v5 + with: + path: shieldlabs-react + persist-credentials: false + - uses: actions/checkout@v5 + with: + repository: ShieldLabs-ai/shieldlabs-js + ref: main + path: shieldlabs-js + persist-credentials: false + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + cache-dependency-path: | + shieldlabs-react/package-lock.json + shieldlabs-js/package-lock.json + - name: Build and pack @shieldlabs-ai/js + working-directory: shieldlabs-js + run: | + npm ci + npm run build + npm pack + - run: npm ci + - run: npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-*.tgz + - run: npm run build + - run: npm pack + - name: Build examples/vite against both packed packages + working-directory: shieldlabs-react/examples/vite + run: | + npm install --no-save --no-package-lock ../../shieldlabs-ai-react-*.tgz ../../../shieldlabs-js/shieldlabs-ai-js-*.tgz + npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e8338ab --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,67 @@ +name: Release + +# Publishes @shieldlabs-ai/react to npm when a maintainer pushes a tag such as v1.0.0. +# Needs the NPM_TOKEN repository secret. Safe to re-run: a version that is already on npm is skipped. +on: + push: + tags: ['v*'] + +permissions: + contents: read + +jobs: + check: + name: Check and pack + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + - name: Check that the tag matches package.json + run: test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME" + - run: npm ci + # Fails until @shieldlabs-ai/js is on npm: publish it first, then re-run this workflow. + - name: Install the published @shieldlabs-ai/js + run: npm install --no-save "@shieldlabs-ai/js@$(node -p "require('./package.json').peerDependencies['@shieldlabs-ai/js']")" + - run: npm run typecheck + - run: npm run lint + - run: npm test -- --coverage + - run: npm run build + - run: npm pack + - uses: actions/upload-artifact@v4 + with: + name: package + path: shieldlabs-ai-react-*.tgz + if-no-files-found: error + + publish: + name: Publish to npm + needs: check + runs-on: ubuntu-latest + # Add required reviewers to this environment in the repository settings to approve each release. + environment: npm + permissions: + contents: read + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: package + - uses: actions/setup-node@v5 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + - name: Publish, unless this version is already on npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + version="${GITHUB_REF_NAME#v}" + if [ "$(npm view "@shieldlabs-ai/react@$version" version 2>/dev/null)" = "$version" ]; then + echo "@shieldlabs-ai/react@$version is already on npm." + else + npm publish "./shieldlabs-ai-react-$version.tgz" --provenance --access public + fi diff --git a/.gitignore b/.gitignore index c544c96..9ea5e3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,19 @@ +# Dependencies and build output node_modules/ dist/ build/ -*.log +coverage/ *.tgz +*.log +.turbo/ + +# Local environment .env .env.* +!.env.example + +# Editors and OS .DS_Store -coverage/ -.turbo/ -.history +.history/ +.idea/ +.vscode/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..45da7c1 --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +# Development settings only: npm never includes .npmrc in the published package. +# @shieldlabs-ai/js, a peer dependency, is not on npm yet, so npm cannot install it by itself. +# legacy-peer-deps: npm ci and npm install leave peer dependencies to you. +# save-dev: `npm install --no-save ` still installs a local pack of @shieldlabs-ai/js. +# Remove this file once @shieldlabs-ai/js 1.x is published (see CONTRIBUTING.md). +legacy-peer-deps=true +save-dev=true diff --git a/CHANGELOG.md b/CHANGELOG.md index ca512e6..0f746a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog -## 2026-09-06 +All notable changes to `@shieldlabs-ai/react` are documented in this file. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). -- Minor improvements and bug fixes +## [Unreleased] + +## [1.0.0] - 2026-09-30 + +### Added + +- `ShieldLabsProvider` with the props `publicKey`, `environment`, `scriptUrl`, `timeout`, + `autoLoad` and `checkOnLoad`: loads the agent once with `load()` of `@shieldlabs-ai/js`, in an effect + after the first render, and reports `status` (`loading`, `ready` or `error`) and `error`. Invalid + props and pages that are not a secure context also log a console warning. Changing an option + loads the agent for the new options. +- `autoLoad={false}` defers loading, for example until consent: nothing loads until `load()` is + called or `autoLoad` becomes `true`. Until then `useIdentify().identify()` resolves `null` at + once with a `not_initialized` error, and `runOnMount` ends the same way without running again + after `load()`. `useShieldLabs().identify()` rejects with `not_initialized`, `check()` resolves + `null` and `getAgent()` waits, with no timeout of its own. +- `useShieldLabs()`: `status`, `error`, `identify()`, `check()`, `load()` and `getAgent()` of the + closest provider. `getAgent()` resolves the loaded agent of `@shieldlabs-ai/js`, for early + identification with `agent.identifyOnInteraction(form)`. Calls made while the agent loads wait + for it, and the call timeout covers the whole call: that wait, then the agent's answer in the + time that is left. A failed or timed-out load is tried again by the next call. +- `useIdentify({ userId, runOnMount })`: `identify()`, `result`, `isLoading`, `error` and + `reset()`. `identify()` resolves the result, or `null` with the reason in `error`, and never + rejects. A call with the same User HID and `timeout` as a running call of the same hook returns + that call, so a double submit costs one identification; a call without `timeout` counts as one + with the provider `timeout`. Options of `identify()` with a `userId` key override the hook + option, also with `undefined` or `null`, which both identify anonymously; only options without + the key use the User HID of the hook. The state follows the call made last. +- `checkOnLoad` runs `check()` once per provider mount when the agent is ready, unless an + `identify()` or `check()` for the same User HID is running at that moment, and `runOnMount` runs + `identify()` once per component mount. Both run once in StrictMode. +- Server rendering that touches neither `window` nor `document`, no state updates after unmount, + state that stays current inside a hidden `` (React 19.2 and later), and hooks outside + the provider throw an error that names the hook. +- Re-exports of `ShieldLabsError` and the types `IdentifyOptions`, `IdentifyResult`, + `InteractionIdentifier`, `LoadOptions`, `ShieldLabsAgent` and `ShieldLabsErrorCode` from + `@shieldlabs-ai/js`. +- ESM, CommonJS and TypeScript declarations with a `"use client"` directive. Peer dependencies: + `react` 18 or 19 and `@shieldlabs-ai/js` 1.x. +- `examples/vite`: a signup form that starts an identification on the first interaction with + `identifyOnInteraction()` and sends the `requestId` with the submit. + +### Removed + +- The placeholder `useShieldLabs(options)` hook of the pre-release scaffold, which returned `data`. + The browser receives a request ID; results are read on your server. + +[Unreleased]: https://github.com/ShieldLabs-ai/shieldlabs-react/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/ShieldLabs-ai/shieldlabs-react/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1f258de --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ +# Contributing to @shieldlabs-ai/react + +Thank you for improving the ShieldLabs React bindings. + +## Set up + +You need Node.js 20 or later. The package builds on `@shieldlabs-ai/js`, a peer dependency. Until +`@shieldlabs-ai/js` 1.0.0 is published to npm, work with a local pack of it, built from a working copy +of [shieldlabs-js](https://github.com/ShieldLabs-ai/shieldlabs-js) next to this repository: + +```bash +# in shieldlabs-js +npm ci && npm run build && npm pack + +# in this repository +npm ci +npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz +``` + +Repeat the last command after every `npm ci`. `--no-save` leaves `package.json` and +`package-lock.json` unchanged. Never commit a `file:` dependency or a tarball. When the pack is +missing, `npm run typecheck`, `npm run lint` and `npm run build` stop with these instructions +(`scripts/check-core.mjs`). + +### Why the repository has an `.npmrc` + +npm installs required peer dependencies by itself. It would try to download `@shieldlabs-ai/js` from +the registry, where it does not exist yet, so `npm ci` would fail. The committed `.npmrc` sets +`legacy-peer-deps=true`, so `npm ci` and `npm install` leave peer dependencies to you, and +`save-dev=true`, so `npm install --no-save ` still installs the pack. Both settings apply +only to work in this repository: npm never publishes `.npmrc`, and `package.json` lists +`@shieldlabs-ai/js` as a required peer dependency, so npm installs it for the users of the package. + +Once `@shieldlabs-ai/js` 1.0.0 is on npm, `npm install --no-save @shieldlabs-ai/js` works instead of the +pack. To tidy up later, delete `.npmrc`, add `@shieldlabs-ai/js` to `devDependencies`, run +`npm install` to refresh `package-lock.json`, and remove the pack steps from the CI workflow and +from this file. + +## Checks + +Run these before you open a pull request. CI runs them on Node.js 20, 22 and 24 with React 18 +and 19. + +```bash +npm run typecheck +npm run lint +npm test -- --coverage # builds first; coverage must stay at 90 % or more +npm run build +``` + +To test with React 18 locally (afterwards, `npm ci` and the pack install bring back React 19): + +```bash +npm install --no-save react@18 react-dom@18 @types/react@18 @types/react-dom@18 ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz +npm run typecheck && npm test +``` + +## Guidelines + +- Stay a thin layer over `@shieldlabs-ai/js`: load the agent only through its `load()`, never touch + `window` or `document` during render, and never identify on re-renders or route changes. +- Every change comes with tests. The tests mock `load()` with the stand-in agent in + `test/support/agent.ts`, and any `console.error` (such as a React update outside `act()`) fails + a test. +- Use conventional commit messages (`feat:`, `fix:`, `docs:`, `test:`, `ci:`, `chore:`) and add a + line to `CHANGELOG.md` under "Unreleased". +- Documentation style: plain technical English, "risk signals", and the three risk bands trusted + 0-29, suspicious 30-59 and dangerous 60-100. + +## Releasing + +Publish `@shieldlabs-ai/js` first. Then update the version in `package.json`, move the "Unreleased" +changelog entries under the new version, and push a tag such as `v1.0.1`. The release workflow runs +all checks against the published `@shieldlabs-ai/js`, packs the package and publishes the pack to npm +with provenance, using the `NPM_TOKEN` repository secret. The publish job runs in the `npm` +environment: add required reviewers to it in the repository settings to approve each release. + +Re-running the workflow is safe. When it stopped because `@shieldlabs-ai/js` was not on npm yet, +publish it and re-run the workflow; a version that is already on npm is not published again. + +## Security + +Please report security issues privately to rather than in a public issue. diff --git a/LICENSE b/LICENSE index 39bb7e4..597e96b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 ShieldLabs +Copyright (c) 2026 ShieldLabs Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9f52ba2..503d76c 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,514 @@ -# @shieldlabs/react +# @shieldlabs-ai/react -React bindings for the [ShieldLabs browser loader](https://github.com/ShieldLabs-ai/shieldlabs-js). +React bindings for ShieldLabs device intelligence: a provider that loads the ShieldLabs agent once, +and hooks that return a request ID for every identification, with loading and error state. -> **Pre-launch.** This package is a placeholder to reserve the name and shape the public API. It is not published yet and the surface will change. Follow along at [shieldlabs.ai](https://shieldlabs.ai). +[![CI](https://github.com/ShieldLabs-ai/shieldlabs-react/actions/workflows/ci.yml/badge.svg)](https://github.com/ShieldLabs-ai/shieldlabs-react/actions/workflows/ci.yml) +[![npm](https://img.shields.io/npm/v/@shieldlabs-ai/react)](https://www.npmjs.com/package/@shieldlabs-ai/react) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) + +`@shieldlabs-ai/react` is a thin layer over [`@shieldlabs-ai/js`](https://github.com/ShieldLabs-ai/shieldlabs-js), +the browser loader that imports the hosted agent from `https://cdn.shieldlabs.ai` at runtime. It +supports React 18 and 19, renders on the server without touching browser globals, and loads the +agent once per app, also in StrictMode. + +New to ShieldLabs? [Start free](https://app.shieldlabs.ai), then copy the Public Key of your domain +from Integration > API keys in the analytics dashboard (the Install tab also shows a ready snippet +that contains it). + +## How it fits + +1. **Browser.** `ShieldLabsProvider` loads the agent, and `useIdentify()` runs an identification for + a protected action. The page receives a `requestId`. +2. **Your backend.** It receives the `requestId` with the protected action (signup, login, + checkout) and reads the verdict for it from the History API with a ShieldLabs server SDK, or + receives it in a signed `identification.scored` webhook. +3. **Decision.** Your backend acts on the Risk Score (bands: trusted 0-29, suspicious 30-59, + dangerous 60-100), the detection flags and identifiers such as the device ID. + +The browser only ever gets the request ID. The Risk Score, risk signals, detection flags, visitor ID +and device ID are read on your server, with one of the server SDKs: +[Node.js](https://github.com/ShieldLabs-ai/shieldlabs-node), +[Python](https://github.com/ShieldLabs-ai/shieldlabs-python), +[Go](https://github.com/ShieldLabs-ai/shieldlabs-go), +[PHP](https://github.com/ShieldLabs-ai/shieldlabs-php), +[Java](https://github.com/ShieldLabs-ai/shieldlabs-java) or +[.NET](https://github.com/ShieldLabs-ai/shieldlabs-dotnet). + +The `identification.scored` webhook is delivered once per identification today (1-second timeout, +no retries). Use the History API when you need a guaranteed read, and make webhook handlers +idempotent on `data.request_id`, because future retries will resend identical bytes. ## Install ```bash -npm install @shieldlabs/react # coming soon +npm install @shieldlabs-ai/react @shieldlabs-ai/js +# or +yarn add @shieldlabs-ai/react @shieldlabs-ai/js +# or +pnpm add @shieldlabs-ai/react @shieldlabs-ai/js ``` -## Usage (subject to change) +`@shieldlabs-ai/js` (1.x) and `react` (18 or 19) are peer dependencies. + +## Quick start + +The snippets use Vite with TypeScript; other bundlers expose environment variables their own way. +Put the Public Key of your domain in `.env` (the value below is a placeholder): + +```bash +# .env +VITE_SHIELDLABS_PUBLIC_KEY=0123456789abcdef0123456789abcdef +``` + +Render `ShieldLabsProvider` once, near the root of your app, around the components that identify: ```tsx -import { useShieldLabs } from "@shieldlabs/react"; +// main.tsx +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { ShieldLabsProvider } from '@shieldlabs-ai/react'; +import { SignupForm } from './SignupForm'; + +createRoot(document.getElementById('root')!).render( + + + + + , +); +``` + +Run an identification when the user submits a protected action, and send the `requestId` with it: -function App() { - const { data, isLoading } = useShieldLabs({ apiKey: "pk_live_..." }); - if (isLoading) return null; - return risk score: {data?.riskScore}; +```tsx +// SignupForm.tsx +import type { SyntheticEvent } from 'react'; +import { useIdentify } from '@shieldlabs-ai/react'; + +export function SignupForm() { + const { identify, isLoading } = useIdentify(); + + async function onSubmit(event: SyntheticEvent) { + event.preventDefault(); + const email = new FormData(event.currentTarget).get('email'); + // null when there is no identification (the reason is in `error`). The signup goes out anyway, + // and your server treats it as unverified. + const result = await identify(); + await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, requestId: result?.requestId ?? null }), + }); + } + + return ( +
+ + +
+ ); } ``` -## About ShieldLabs +`identify()` never rejects: it resolves the result, or `null` with the reason in `error`. A second +submit while the identification runs gets the same one, so a double click costs one identification. + +On your server, read the verdict for `requestId` with a server SDK, for example +`identifications.get(requestId)` in [`@shieldlabs-ai/node`](https://github.com/ShieldLabs-ai/shieldlabs-node), +which waits until the identification has been scored. The History row appears about 1-3 seconds +after `identify()` resolves and can be refined for up to about 10 seconds as follow-up checks +finish, so starting the identification when the user begins the action (see +[Protect a form](#protect-a-form)) gets your server the verdict sooner. Accept each request ID once +and only within your freshness window (the examples use 5 minutes): one identification authorizes +one protected action. + +> **Keep the page alive after `identify()` resolves.** The agent posts the identification right +> after it hands over the request ID. Sending your request with `fetch()`, as above, keeps the page +> open. If you navigate right after the submit (a full-page form post or a redirect), start the +> identification early instead (see [Protect a form](#protect-a-form)). + +> **Test on a registered domain.** ShieldLabs records identifications only for the domains +> registered in your account. On `localhost` the page still receives a `requestId`, but the +> identification is rejected with `401` and your backend never finds it. Test on a development +> domain with its own keys, as described in [Environments](https://docs.shieldlabs.ai/setup/environments). + +## Guide + +### Protect a form + +`identify()` on submit, as in the quick start, is enough for most single-page apps. To have the +identification finished by the time the user submits, start it on the first interaction with the +form. `getAgent()` from `useShieldLabs()` resolves the loaded agent of `@shieldlabs-ai/js`, and its +`identifyOnInteraction(form)` starts `identify()` on the first `focusin`, `pointerdown` or `keydown` +inside the form. The handle's `take()` returns that identification for this submission and re-arms, +so the next submission gets its own request ID: + +```tsx +import { useEffect, useRef, type SyntheticEvent } from 'react'; +import { useIdentify, useShieldLabs, type InteractionIdentifier } from '@shieldlabs-ai/react'; + +export function SignupForm() { + const { getAgent } = useShieldLabs(); + const { identify } = useIdentify(); + const formRef = useRef(null); + const early = useRef(null); + + useEffect(() => { + const form = formRef.current; + if (!form) return; + let active = true; + getAgent().then( + (agent) => { + if (active) early.current = agent.identifyOnInteraction(form); + }, + () => {}, // the agent could not load: the submit handler tries again + ); + return () => { + active = false; + early.current?.dispose(); + early.current = null; + }; + }, [getAgent]); + + async function onSubmit(event: SyntheticEvent) { + event.preventDefault(); + const email = new FormData(event.currentTarget).get('email'); + // The early identification while it is fresh, otherwise a new one. Without an early handle, + // identify() loads the agent again; it resolves null when there is no identification. + const result = early.current ? await early.current.take().catch(() => null) : await identify(); + await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, requestId: result?.requestId ?? null }), + }); + } + + return ( +
+ + +
+ ); +} +``` + +For a classic full-page post, put the request ID in a hidden field +(`` in a `
`) and submit +the form yourself: + +```tsx +async function onSubmit(event: SyntheticEvent) { + event.preventDefault(); + const form = event.currentTarget; + const result = await early.current?.take().catch(() => null); + (form.elements.namedItem('requestId') as HTMLInputElement).value = result?.requestId ?? ''; + form.submit(); +} +``` + +Because the identification starts on the first interaction, it has normally finished posting by the +time the user submits. If users can submit without interacting first (for example autofill and a +single click on the button), prefer sending the form with `fetch()`, which keeps the page alive. + +`take()` hands out the early identification only while it is fresh. When it failed, or finished more +than 4 minutes ago, `take()` starts a new one, so the request ID your server receives stays inside a +5-minute freshness window. While users keep interacting with the form, a new identification starts +at most every 4 minutes (after a failure, at most one attempt every 5 seconds), and each of them is +billed. The effect's cleanup removes the listeners when the form unmounts. With +`autoLoad={false}` (see [Consent](#consent)), `getAgent()` waits for `load()`, so the form is armed +once `load()` has been called and the agent has loaded. + +[`examples/vite`](./examples/vite) is a complete signup form built this way. + +### Signed-in users: pass a User HID + +Pass a User HID so ShieldLabs ties the identification to the account. Compute it **on your server** +from your account ID with a secret key, for example with the `userHid(userId, secret)` helper of +the server SDKs (HMAC-SHA256, 64 hex characters), and hand it to the page, for example in your +session data: + +```tsx +const { identify } = useIdentify({ userId: session.userHid }); +// Later, for the protected action. identify({ userId }) overrides the User HID for one call. +const result = await identify(); // result?.userId is the User HID that was sent +``` + +Options of `identify()` with a `userId` key override the User HID of the hook for that call, also +when the value is `undefined` or `null`: `identify({ userId: undefined })` identifies anonymously. +Only options without the key use the User HID of the hook. + +Never pass a raw email address, phone number or database ID. Omit `userId` for visitors who are not +signed in. The rules for the value (reserved values, characters that are hard to search) are in the +[`@shieldlabs-ai/js` guide](https://github.com/ShieldLabs-ai/shieldlabs-js#signed-in-users-pass-a-user-hid). + +### Identify when a component mounts + +`useIdentify({ runOnMount: true })` runs `identify()` once when the component mounts, as soon as the +agent is ready. `isLoading` is `true` from the first render. Re-renders, prop changes and StrictMode +do not run it again; a new mount of the component does. With `autoLoad={false}`, a mount before +`load()` ends at once with a `not_initialized` error and does not run again after `load()` (see +[Consent](#consent)). Every run is a billable identification, so use it for a component that is +itself the protected step, never in a layout, a list item or a component that mounts on every route. + +`result` is one identification, and one identification authorizes one protected action: send its +`requestId` with a single request, within your freshness window (5 minutes in the examples). Your +server rejects a request ID it has seen before or one that is too old. For every later action (a +retry after a declined card, a second submit, a user who comes back after a break), call +`identify()` again. For form submits, `identify()` in the submit handler, as in the quick start, is +the simpler choice. + +```tsx +function RecoveryStep({ userHid }: { userHid: string }) { + const { result, isLoading } = useIdentify({ userId: userHid, runOnMount: true }); + if (isLoading) return

Loading

; + // The request ID goes with the one request that loads the recovery options. Without a result (the + // identification failed), that request is sent without a requestId. + return ; +} +``` + +### Background checks with `checkOnLoad` + +`checkOnLoad` runs `check()` once per provider mount when the agent is ready, for passive monitoring +of the visit. `true` checks anonymously, `{ userId }` passes a User HID: -ShieldLabs gives you identification and anonymity detection with an explainable risk score (0-100) and detailed signals, so you can assess traffic quality and act on abuse and fraud in your own code. You read the score and its details; your code owns the decision. You set the rules. +```tsx + + + +``` + +The agent limits `check()` to one identification per visit every five minutes, shared across tabs. +The check uses the value of `checkOnLoad` at the moment the agent becomes ready, and changes after +that do not run it again. It is skipped when an `identify()` or `check()` for the same User HID is +still running at that moment (for example `runOnMount`, or a submit made while the agent loaded): +that call already identifies the visit, and the agent runs one identification at a time for a User +HID. A skipped or failed check is ignored (invalid options log a console warning). Your backend sees +these identifications like any other; to get the request ID in the page, call `check()` from +`useShieldLabs()` instead, which resolves `null` when the agent skipped it. + +### Loading and error state + +`useShieldLabs()` returns the agent status, the provider's `identify()` and `check()`, `load()` +and `getAgent()`: + +```tsx +function AgentStatus() { + const { status, error } = useShieldLabs(); + if (status === 'error') return Identification is unavailable ({error?.code}).; + return null; +} +``` + +You do not need to wait for `'ready'`: `identify()` and `check()` called while the agent loads wait +for it. The call's `timeout` (default: the provider `timeout`, 10 seconds) covers the whole call: +that wait and then the agent's answer, which gets only the time that is left. Never block a +protected action on the status. When the agent cannot load (a content blocker, a network error), +send the action without a `requestId`; your backend treats it as unverified. A failed or timed-out +load is tried again by the next `identify()`, `check()`, `getAgent()` or `load()`, and the status +follows. When the provider props are invalid (for example a missing `publicKey` because an +environment variable is not set) or the page is not a secure context, the provider also logs a +console warning that starts with `[ShieldLabs]`. + +`identify()` and `check()` of `useShieldLabs()` are the calls of `@shieldlabs-ai/js`: `identify()` +rejects with a `ShieldLabsError` when there is no identification. `useIdentify()` wraps it with +`result`, `isLoading` and `error`, never rejects and shares a running call. + +### Server-side rendering and StrictMode + +- The provider and the hooks render on the server. Nothing touches `window` or `document` during + render; the agent loads in an effect after hydration (with `autoLoad={false}`, once `load()` is + called). The server HTML shows `status: 'loading'` (and `isLoading: true` for `runOnMount`). +- In StrictMode the provider loads the agent once, and `runOnMount` and `checkOnLoad` run once per + mount. `load()` of `@shieldlabs-ai/js` is memoized per agent URL and Public Key, so mounting the + provider again reuses the agent that is already loaded. +- The hooks never identify on re-renders or on route changes. Keep the provider above your router so + that navigation does not remount it. +- Inside `` (React 19.2 and later) the provider and the hooks keep their + state. A load or an identification that finishes while the content is hidden shows up when it is + visible again, and showing it again does not run `runOnMount` or `checkOnLoad` again. +- The built files start with the `"use client"` directive, so bundlers for React Server Components + treat the package as client code. + +### Next.js + +Use [`@shieldlabs-ai/next`](https://github.com/ShieldLabs-ai/shieldlabs-next). It provides this +provider and these hooks as a client module for the App Router (the Pages Router is documented +there) and adds server helpers in `@shieldlabs-ai/next/server` for reading identifications and +verifying webhooks in route handlers. + +### Call budget and Content Security Policy + +The rules of `@shieldlabs-ai/js` apply unchanged: + +- [Call budget](https://github.com/ShieldLabs-ai/shieldlabs-js#call-budget): one identification per + protected action, and a small per-IP budget on the ingest. Never clear the agent's storage. +- [Content Security Policy](https://github.com/ShieldLabs-ai/shieldlabs-js#content-security-policy): + the `script-src` and `connect-src` origins the agent needs. + +### Consent + +The agent does not read your consent banner (see +[Consent](https://github.com/ShieldLabs-ai/shieldlabs-js#consent) in the `@shieldlabs-ai/js` guide). +Where your policy requires consent before the agent loads, render the provider with +`autoLoad={false}`: nothing loads until `load()` from `useShieldLabs()` is called, or until +`autoLoad` becomes `true`. + +```tsx +import { ShieldLabsProvider, useShieldLabs } from '@shieldlabs-ai/react'; +import { SignupForm } from './SignupForm'; + +export function App() { + return ( + + + + + ); +} + +function ConsentBanner() { + const { load } = useShieldLabs(); + // Record the choice as your consent tool requires, then load the agent. + return ; +} +``` + +When your consent state lives in React already, pass it instead: +``. Once loading has started, +calls wait for the agent as usual. + +Until then, the rest of the app works unchanged and nothing waits for consent: + +- `identify()` from `useIdentify()` resolves `null` at once, with a `not_initialized` error, so + forms go out without a `requestId` and your server treats them as unverified. `runOnMount` ends + the same way and does not run again by itself after `load()`. +- `useShieldLabs().identify()` rejects with `not_initialized`, and `check()` resolves `null`. +- `getAgent()` waits, with no timeout of its own, so a form set up for early identification (see + [Protect a form](#protect-a-form)) is armed once `load()` has been called and the agent has + loaded. When that load fails, `getAgent()` rejects with its error. `checkOnLoad` runs once the + agent is ready. +- `status` stays `'loading'`. + +Setting `autoLoad` back to `false` does not unload an agent that has loaded. + +## Reference + +| Export | Description | +|---|---| +| `ShieldLabsProvider` | Loads the agent once and provides it to the hooks below it | +| `useShieldLabs()` | Agent status, load error, `identify()`, `check()`, `load()` and `getAgent()` of the closest provider | +| `useIdentify(options?)` | Identification with `result`, `isLoading`, `error` and `reset()`. Its `identify()` resolves `null` instead of rejecting | +| `ShieldLabsError` | The error class of `@shieldlabs-ai/js` (re-exported). Has `code` and optional `cause` | +| Types | `ShieldLabsProviderProps`, `ShieldLabsStatus`, `UseShieldLabsResult`, `UseIdentifyOptions`, `UseIdentifyResult`, and from `@shieldlabs-ai/js`: `IdentifyOptions`, `IdentifyResult`, `InteractionIdentifier`, `LoadOptions`, `ShieldLabsAgent`, `ShieldLabsErrorCode` | + +`` props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `publicKey` | `string` | required | Public Key of your domain | +| `environment` | `'production' \| 'development'` | `'production'` | Which ShieldLabs CDN to load the agent from | +| `scriptUrl` | `string` | | Advanced: agent module URL override (`https`, or `http` on `localhost` and `127.0.0.1`) | +| `timeout` | `number` | `10000` | Milliseconds to wait for the agent to load, and the default timeout of each `identify()` and `check()` call | +| `autoLoad` | `boolean` | `true` | Loads the agent after the first render. With `false`, nothing loads until `load()` is called or `autoLoad` becomes `true` (see [Consent](#consent)) | +| `checkOnLoad` | `boolean \| { userId?: string }` | `false` | Runs `check()` once per provider mount when the agent is ready, unless a call for the same User HID is running | +| `children` | `ReactNode` | | Your app | + +Changing `publicKey`, `environment`, `scriptUrl` or `timeout` loads the agent for the new options +(once loading is allowed), and the status goes back to `'loading'`. + +`useShieldLabs()` returns + +| Field | Type | Description | +|---|---|---| +| `status` | `'loading' \| 'ready' \| 'error'` | `'loading'` until the agent has loaded, then `'ready'`, or `'error'` when loading failed | +| `error` | `ShieldLabsError \| null` | Why loading failed while `status` is `'error'` | +| `identify(options?)` | `Promise` | Fresh identification, a new request ID on every call. Waits for the agent while it loads. Rejects with a `ShieldLabsError` | +| `check(options?)` | `Promise` | Background check, limited by the agent to one per visit every five minutes. `null` when skipped | +| `load()` | `void` | Starts loading the agent: needed only with `autoLoad={false}`. Also loads again after a failed load. Safe to call more than once; call it from an event handler or an effect | +| `getAgent()` | `Promise` | The loaded agent of `@shieldlabs-ai/js`, for example for `identifyOnInteraction(form)`. Waits while the agent loads (with `autoLoad={false}`, until `load()`), with no timeout of its own. Rejects with the load error | + +`useIdentify(options?)` + +| Option | Type | Default | Description | +|---|---|---|---| +| `userId` | `string` | | User HID for every identification of this hook. Options of `identify()` with a `userId` key override it, also with `undefined` or `null` (an anonymous identification) | +| `runOnMount` | `boolean` | `false` | Runs `identify()` once when the component mounts, as soon as the agent is ready | + +| Field | Type | Description | +|---|---|---| +| `identify(options?)` | `Promise` | Starts a new identification and resolves its result, or `null` when there is none (the reason is in `error`). Never rejects. While a call of this hook with the same User HID and `timeout` runs, returns that call instead of starting another (a call without `timeout` counts as one with the provider `timeout`) | +| `result` | `IdentifyResult \| null` | Result of the latest identification. `null` while a new one runs, when it failed and after `reset()` | +| `isLoading` | `boolean` | `true` while the latest identification runs | +| `error` | `ShieldLabsError \| null` | Why the latest identification failed | +| `reset()` | `void` | Clears `result` and `error`. A running identification no longer updates the state, and the next `identify()` starts a new one | + +The state follows the call made last. A call with another User HID or `timeout` than a running one +starts its own identification. The User HID of a call is the `userId` of its options when they have +that key (`undefined` and `null` both mean anonymous), else the `userId` of the hook: in a hook with +a User HID, `identify()` and `identify({ userId: undefined })` are two identifications. A call +without `timeout` counts as one with the provider `timeout` (10 seconds by default), so `identify()` +and `identify({ timeout: 10000 })` share one identification. Two `useIdentify()` hooks never share +a call. The functions keep their identity across renders (`identify` changes when `userId` +changes), so they are safe in effect dependencies. + +`IdentifyOptions` (from `@shieldlabs-ai/js`) + +| Option | Type | Description | +|---|---|---| +| `userId` | `string` | User HID computed on your server. Omit for anonymous checks | +| `timeout` | `number` | Milliseconds the whole call may take: a wait for the agent to load, then the agent's answer in the time that is left. Overrides the provider `timeout`, which also limits the load itself | + +`IdentifyResult` (from `@shieldlabs-ai/js`) + +| Field | Type | Description | +|---|---|---| +| `requestId` | `string` | Send it to your backend with the protected action | +| `userId` | `string \| null` | The User HID used, `null` for anonymous checks | + +## Errors and retries + +Every error is a `ShieldLabsError`. `useIdentify().identify()` stores it in `error` and resolves +`null`; the calls of `useShieldLabs()` reject with it. Branch on `error.code`: + +| `code` | When | What happens and what to do | +|---|---|---| +| `invalid_options` | A provider prop or a call option failed validation, or `publicKey` holds a server-side secret | A bad prop sets `status` to `'error'` and logs a console warning; a bad call option fails that call. Fix the value; retrying does not help | +| `unsupported_environment` | The page is not a secure context | `status` is `'error'`, with a console warning. Serve the page over HTTPS (`localhost` and `127.0.0.1` also work over `http`) | +| `load_failed` | The agent module could not be imported (network error, content blocker, Content Security Policy) | `status` is `'error'`. Continue without an identification; the next `identify()`, `check()`, `getAgent()` or `load()` loads again | +| `timeout` | The agent did not load, or an agent call did not answer, within the timeout (default 10 seconds) | Continue without an identification. A load that timed out keeps running, and the next call uses it once it arrives | +| `not_initialized` | `identify()` only: the agent did not start an identification, for example because another one is running in this or another tab, or the provider has `autoLoad={false}` and `load()` has not been called. `check()` resolves `null` instead | Retry once later, or continue without an identification | + +Whenever there is no identification, send the protected action anyway without a `requestId`: your +backend treats a missing identification as unverified (for example step-up or review), never as +clean. Calling a hook outside `ShieldLabsProvider` throws an `Error` that names the hook. + +## Compatibility + +- React 18 and 19 (React DOM), with TypeScript types for both. +- Browsers: the same as `@shieldlabs-ai/js` (ES modules, dynamic `import()` and WebCrypto, in a secure + context). +- Server rendering with `react-dom/server` in Node.js 18 or later; the agent loads only in the + browser. +- Output: ES2019 syntax as ESM and CommonJS with TypeScript declarations, marked `"use client"`. + No dependencies besides the peer dependencies. + +## Development + +```bash +npm ci +# Until @shieldlabs-ai/js is on npm, install a local pack of it (see CONTRIBUTING.md): +npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz +npm run typecheck +npm run lint +npm test -- --coverage # builds first, then runs the tests +npm run build +``` -- Website: [shieldlabs.ai](https://shieldlabs.ai) -- Get started: [Start Free](https://shieldlabs.ai) +See [CONTRIBUTING.md](./CONTRIBUTING.md). Documentation: . Analytics +dashboard: . Support: . ## License -[MIT](./LICENSE) +[MIT](./LICENSE), Copyright (c) 2026 ShieldLabs Inc. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c791656 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,37 @@ +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import reactHooks from 'eslint-plugin-react-hooks'; +import tseslint from 'typescript-eslint'; + +export default defineConfig( + { ignores: ['dist/', 'coverage/', 'examples/', 'node_modules/'] }, + js.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + reactHooks.configs.flat.recommended, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: fileURLToPath(new URL('.', import.meta.url)), + }, + }, + rules: { + '@typescript-eslint/restrict-plus-operands': ['error', { allowNumberAndString: false }], + }, + }, + { + files: ['**/*.js', '**/*.mjs'], + extends: [tseslint.configs.disableTypeChecked], + languageOptions: { + globals: { URL: 'readonly', console: 'readonly', process: 'readonly' }, + }, + }, + { + files: ['test/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + }, + }, +); diff --git a/examples/vite/.env.example b/examples/vite/.env.example new file mode 100644 index 0000000..99de330 --- /dev/null +++ b/examples/vite/.env.example @@ -0,0 +1,2 @@ +# Copy to .env and set the Public Key of your domain. +VITE_SHIELDLABS_PUBLIC_KEY=0123456789abcdef0123456789abcdef diff --git a/examples/vite/README.md b/examples/vite/README.md new file mode 100644 index 0000000..355a331 --- /dev/null +++ b/examples/vite/README.md @@ -0,0 +1,48 @@ +# React and Vite example + +A signup form in React. `ShieldLabsProvider` loads the ShieldLabs agent once, the form starts an +identification on the user's first interaction with it, and the submit handler sends the `requestId` +to your backend with the signup request. + +`src/SignupForm.tsx` gets the loaded agent with `getAgent()` from `useShieldLabs()` and calls +`agent.identifyOnInteraction(form)` in an effect. The first focus, click or key press in the form +starts an identification, so it is usually finished by the time the user submits. The submit +handler calls `take()`, which hands over that identification while it is fresh and has not failed, +and otherwise starts a new one; the next submission gets its own. While a user keeps interacting +with the form, a new identification starts at most every four minutes (the server accepts a request +ID for five minutes in these examples), and each one is billable. The effect's cleanup calls +`dispose()`. When the agent could not load at first, the submit handler calls `identify()` from +`useIdentify()`, which loads it again. + +## Run it + +```bash +npm install +cp .env.example .env # then set VITE_SHIELDLABS_PUBLIC_KEY +npm run dev +``` + +Open the URL Vite prints (for example ). The form posts JSON to +`/api/signup`: point it at your server, which reads the verdict for `requestId` with a ShieldLabs +server SDK (for example `identifications.get(requestId)` in `@shieldlabs-ai/node`). + +ShieldLabs accepts identifications only from registered domains. On `localhost` the request ID +still reaches the page, but no identification is recorded. Serve the example from a registered +development domain to see results in the [analytics dashboard](https://app.shieldlabs.ai). + +## Build against local copies of the packages + +Before `@shieldlabs-ai/react` and `@shieldlabs-ai/js` are on npm, with both repositories side by side: + +```bash +# in shieldlabs-js +npm ci && npm run build && npm pack + +# in shieldlabs-react +npm ci +npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz +npm run build && npm pack +cd examples/vite +npm install --no-save --no-package-lock ../../shieldlabs-ai-react-1.0.0.tgz ../../../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz +npm run build +``` diff --git a/examples/vite/index.html b/examples/vite/index.html new file mode 100644 index 0000000..07fbe1a --- /dev/null +++ b/examples/vite/index.html @@ -0,0 +1,18 @@ + + + + + + ShieldLabs signup example (React and Vite) + + + +
+ + + diff --git a/examples/vite/package.json b/examples/vite/package.json new file mode 100644 index 0000000..e617eb9 --- /dev/null +++ b/examples/vite/package.json @@ -0,0 +1,24 @@ +{ + "name": "shieldlabs-example-react-vite", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@shieldlabs-ai/js": "^1.0.0", + "@shieldlabs-ai/react": "^1.0.0", + "react": "^19.3.0", + "react-dom": "^19.3.0" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "~5.9.3", + "vite": "^6.4.3" + } +} diff --git a/examples/vite/src/SignupForm.tsx b/examples/vite/src/SignupForm.tsx new file mode 100644 index 0000000..7bc10a7 --- /dev/null +++ b/examples/vite/src/SignupForm.tsx @@ -0,0 +1,98 @@ +import { useEffect, useRef, useState, type SyntheticEvent } from 'react'; +import { ShieldLabsError, useIdentify, useShieldLabs, type InteractionIdentifier } from '@shieldlabs-ai/react'; + +function describe(error: unknown): string { + return error instanceof ShieldLabsError ? `ShieldLabs ${error.code}: ${error.message}` : String(error); +} + +export function SignupForm() { + const { status, error, getAgent } = useShieldLabs(); + const { identify } = useIdentify(); + const formRef = useRef(null); + const early = useRef(null); + const [message, setMessage] = useState(''); + const [submitting, setSubmitting] = useState(false); + + // Once the agent has loaded, identifyOnInteraction() starts an identification on the first focus, + // click or key press in the form, so it is usually finished by the time the form is submitted. + useEffect(() => { + const form = formRef.current; + if (!form) return; + let active = true; + getAgent().then( + (agent) => { + if (active) early.current = agent.identifyOnInteraction(form); + }, + () => { + // The agent could not load (the status line shows why). The submit handler tries again. + }, + ); + return () => { + active = false; + early.current?.dispose(); + early.current = null; + }; + }, [getAgent]); + + const submit = async (form: HTMLFormElement): Promise => { + const data = new FormData(form); + setSubmitting(true); + + let requestId: string | null = null; + try { + // One identification per submission: the early one while it is fresh, otherwise a new one. + // When the agent could not load at first, identify() tries to load it again (it resolves null + // instead of rejecting). + const result = early.current ? await early.current.take() : await identify(); + requestId = result?.requestId ?? null; + } catch (reason) { + // Without an identification your server treats the signup as unverified. + setMessage(describe(reason)); + } + + try { + // The page stays open while this request is sent, so the agent can finish posting. + const response = await fetch('/api/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: data.get('email'), password: data.get('password'), requestId }), + }); + setMessage( + response.ok + ? 'Account created.' + : `Your server answered ${String(response.status)}. It would receive requestId ${requestId ?? '(none)'}.`, + ); + } catch (reason) { + setMessage(`The signup request failed: ${String(reason)}`); + } finally { + setSubmitting(false); + } + }; + + const onSubmit = (event: SyntheticEvent): void => { + event.preventDefault(); + void submit(event.currentTarget); + }; + + return ( +
+

Create an account

+ + + + + +

{message}

+

+ Agent: {status} + {error ? ` (${error.code})` : ''} +

+
+ ); +} diff --git a/examples/vite/src/main.tsx b/examples/vite/src/main.tsx new file mode 100644 index 0000000..7ff546f --- /dev/null +++ b/examples/vite/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { ShieldLabsProvider } from '@shieldlabs-ai/react'; +import { SignupForm } from './SignupForm'; + +const root = document.getElementById('root'); +if (!root) throw new Error('The #root element is missing.'); + +createRoot(root).render( + + {/* One provider for the whole app: it loads the agent once, also in StrictMode. */} + + + + , +); diff --git a/examples/vite/src/vite-env.d.ts b/examples/vite/src/vite-env.d.ts new file mode 100644 index 0000000..9c13f80 --- /dev/null +++ b/examples/vite/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** The Public Key of your domain (Integration > API keys in the analytics dashboard). */ + readonly VITE_SHIELDLABS_PUBLIC_KEY: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/examples/vite/tsconfig.json b/examples/vite/tsconfig.json new file mode 100644 index 0000000..8872292 --- /dev/null +++ b/examples/vite/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/examples/vite/vite.config.ts b/examples/vite/vite.config.ts new file mode 100644 index 0000000..fabde1a --- /dev/null +++ b/examples/vite/vite.config.ts @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8109394 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4987 @@ +{ + "name": "@shieldlabs-ai/react", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@shieldlabs-ai/react", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@eslint/js": "~9.39.5", + "@testing-library/dom": "^10.4.2", + "@testing-library/react": "^16.3.3", + "@types/node": "^22.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitest/coverage-v8": "~3.2.7", + "eslint": "~9.39.5", + "eslint-plugin-react-hooks": "^7.1.1", + "happy-dom": "^20.14.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "tsup": "^8.5.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.71.0", + "vitest": "~3.2.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@shieldlabs-ai/js": "^1.0.0", + "react": "^18.0.0 || ^19.0.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, + "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.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "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-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, + "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, + "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, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "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, + "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, + "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, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "engines": { + "node": ">=6.9.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, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "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 + }, + "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 + }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", + "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/type-utils": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", + "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", + "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "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" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001813", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001813.tgz", + "integrity": "sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==", + "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" + } + ] + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "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 + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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 + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.441", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.441.tgz", + "integrity": "sha512-b84H5dyxtHtXYbJRJPLvbS+iud4ys2GVZ61BH7/9lJrfCcIL+Sk+Vh2AW6beaErTdstvjZLgEwkE2j0fZlmwRA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/happy-dom": { + "version": "20.14.5", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz", + "integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==", + "dev": true, + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "dependencies": { + "hermes-estree": "0.25.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 + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "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, + "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, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "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, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "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, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "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 + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dev": true, + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.5", + "@rollup/rollup-android-arm64": "4.63.5", + "@rollup/rollup-darwin-arm64": "4.63.5", + "@rollup/rollup-darwin-x64": "4.63.5", + "@rollup/rollup-freebsd-arm64": "4.63.5", + "@rollup/rollup-freebsd-x64": "4.63.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", + "@rollup/rollup-linux-arm-musleabihf": "4.63.5", + "@rollup/rollup-linux-arm64-gnu": "4.63.5", + "@rollup/rollup-linux-arm64-musl": "4.63.5", + "@rollup/rollup-linux-loong64-gnu": "4.63.5", + "@rollup/rollup-linux-loong64-musl": "4.63.5", + "@rollup/rollup-linux-ppc64-gnu": "4.63.5", + "@rollup/rollup-linux-ppc64-musl": "4.63.5", + "@rollup/rollup-linux-riscv64-gnu": "4.63.5", + "@rollup/rollup-linux-riscv64-musl": "4.63.5", + "@rollup/rollup-linux-s390x-gnu": "4.63.5", + "@rollup/rollup-linux-x64-gnu": "4.63.5", + "@rollup/rollup-linux-x64-musl": "4.63.5", + "@rollup/rollup-openbsd-x64": "4.63.5", + "@rollup/rollup-openharmony-arm64": "4.63.5", + "@rollup/rollup-win32-arm64-msvc": "4.63.5", + "@rollup/rollup-win32-ia32-msvc": "4.63.5", + "@rollup/rollup-win32-x64-gnu": "4.63.5", + "@rollup/rollup-win32-x64-msvc": "4.63.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "engines": { + "node": ">= 12" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "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 + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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 + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "engines": { + "node": ">=14.0.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, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", + "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.0", + "@typescript-eslint/parser": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "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" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "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", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json index 71d5602..8810bf7 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,86 @@ { - "name": "@shieldlabs/react", - "version": "0.0.0", - "description": "React bindings for the ShieldLabs browser loader.", + "name": "@shieldlabs-ai/react", + "version": "1.0.0", + "description": "React bindings for ShieldLabs device intelligence: a provider that loads the agent once and hooks that return a request ID for every identification.", + "keywords": [ + "shieldlabs", + "react", + "react-hooks", + "device-intelligence", + "fraud-detection", + "risk-score", + "identification" + ], + "homepage": "https://docs.shieldlabs.ai", + "bugs": { + "url": "https://github.com/ShieldLabs-ai/shieldlabs-react/issues", + "email": "contact@shieldlabs.ai" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ShieldLabs-ai/shieldlabs-react.git" + }, + "license": "MIT", + "author": "ShieldLabs Inc. ", "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "CHANGELOG.md" + ], + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" }, - "files": ["dist", "README.md", "LICENSE"], - "sideEffects": false, "scripts": { - "build": "tsup src/index.ts --format esm,cjs --dts --clean", - "typecheck": "tsc --noEmit" + "prebuild": "node scripts/check-core.mjs", + "build": "tsup", + "pretypecheck": "node scripts/check-core.mjs", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json", + "prelint": "node scripts/check-core.mjs", + "lint": "eslint .", + "pretest": "npm run build", + "test": "vitest run", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "@shieldlabs-ai/js": "^1.0.0", + "react": "^18.0.0 || ^19.0.0" }, - "keywords": ["shieldlabs", "react", "device-intelligence", "visitor-identification", "risk-score"], - "repository": { "type": "git", "url": "git+https://github.com/ShieldLabs-ai/shieldlabs-react.git" }, - "homepage": "https://shieldlabs.ai", - "bugs": { "url": "https://github.com/ShieldLabs-ai/shieldlabs-react/issues" }, - "license": "MIT", - "publishConfig": { "access": "public" }, - "peerDependencies": { "react": ">=17" }, - "dependencies": { "@shieldlabs/js": "workspace:*" }, "devDependencies": { - "react": "^18.2.0", - "@types/react": "^18.2.0", - "tsup": "^8.0.0", - "typescript": "^5.4.0" + "@eslint/js": "~9.39.5", + "@testing-library/dom": "^10.4.2", + "@testing-library/react": "^16.3.3", + "@types/node": "^22.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitest/coverage-v8": "~3.2.7", + "eslint": "~9.39.5", + "eslint-plugin-react-hooks": "^7.1.1", + "happy-dom": "^20.14.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "tsup": "^8.5.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.71.0", + "vitest": "~3.2.7" } } diff --git a/scripts/check-core.mjs b/scripts/check-core.mjs new file mode 100644 index 0000000..77c7eea --- /dev/null +++ b/scripts/check-core.mjs @@ -0,0 +1,31 @@ +// Runs before the type check, lint and build. `npm ci` leaves out the peer dependency @shieldlabs-ai/js +// (see .npmrc), and the tools would fail with "Cannot find module '@shieldlabs-ai/js'". This check names +// the missing step instead (see CONTRIBUTING.md). +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +let version = null; +try { + version = JSON.parse(readFileSync(require.resolve('@shieldlabs-ai/js/package.json'), 'utf8')).version; +} catch { + // Not installed. +} + +if (typeof version !== 'string' || !version.startsWith('1.')) { + const found = typeof version === 'string' ? 'Found @shieldlabs-ai/js ' + version + '.' : 'It is not installed.'; + console.error( + [ + '@shieldlabs-ai/react needs @shieldlabs-ai/js 1.x, a peer dependency. ' + found, + 'Until @shieldlabs-ai/js is on npm, build a pack from a working copy of shieldlabs-js next to this', + 'repository and install it (repeat the install after every npm ci):', + '', + ' (cd ../shieldlabs-js && npm ci && npm run build && npm pack)', + ' npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz', + '', + 'See CONTRIBUTING.md.', + ].join('\n'), + ); + process.exit(1); +} diff --git a/src/index.ts b/src/index.ts index f105056..b62650b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,13 @@ -/** - * @shieldlabs/react — thin React wrapper over @shieldlabs/js. - * No signal-collection logic lives here. - * - * Status: pre-launch scaffold. Hook surface is a placeholder. - */ -import type { IdentificationResult, ShieldLabsOptions } from "@shieldlabs/js"; - -export type { IdentificationResult, ShieldLabsOptions }; - -export interface UseShieldLabsState { - data?: IdentificationResult; - error?: Error; - isLoading: boolean; -} - -/** Placeholder hook. Not implemented yet. */ -export function useShieldLabs(_options: ShieldLabsOptions): UseShieldLabsState { - return { isLoading: false, error: new Error("@shieldlabs/react is not published yet.") }; -} +export { ShieldLabsProvider, useShieldLabs } from './provider'; +export type { ShieldLabsProviderProps, ShieldLabsStatus, UseShieldLabsResult } from './provider'; +export { useIdentify } from './use-identify'; +export type { UseIdentifyOptions, UseIdentifyResult } from './use-identify'; +export { ShieldLabsError } from '@shieldlabs-ai/js'; +export type { + IdentifyOptions, + IdentifyResult, + InteractionIdentifier, + LoadOptions, + ShieldLabsAgent, + ShieldLabsErrorCode, +} from '@shieldlabs-ai/js'; diff --git a/src/internal.ts b/src/internal.ts new file mode 100644 index 0000000..be22fc1 --- /dev/null +++ b/src/internal.ts @@ -0,0 +1,41 @@ +import { ShieldLabsError, type ShieldLabsErrorCode } from '@shieldlabs-ai/js'; + +/** Default time in milliseconds for each agent call, the same as `@shieldlabs-ai/js`. */ +export const DEFAULT_TIMEOUT = 10000; + +/** Largest delay `setTimeout` accepts without overflowing. */ +const MAX_TIMEOUT = 2147483647; + +export function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +export function isValidTimeout(value: unknown): value is number { + return typeof value === 'number' && value > 0 && value <= MAX_TIMEOUT; +} + +/** Keeps a `ShieldLabsError` as it is and wraps anything else, with the original as `cause`. */ +export function asShieldLabsError(error: unknown, code: ShieldLabsErrorCode, message: string): ShieldLabsError { + return error instanceof ShieldLabsError ? error : new ShieldLabsError(code, message, error); +} + +/** + * Settles like `promise`, or rejects with a `timeout` error after `ms` milliseconds. The timer is + * always cleared when `promise` settles first. + */ +export function withTimeout(promise: Promise, ms: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new ShieldLabsError('timeout', message)); + }, ms); + const clear = (): void => { + clearTimeout(timer); + }; + promise.then(clear, clear); + promise.then(resolve, reject); + }); +} + +export function ignore(): void { + // Intentionally empty: the outcome is reported another way. +} diff --git a/src/loader.ts b/src/loader.ts new file mode 100644 index 0000000..4211db1 --- /dev/null +++ b/src/loader.ts @@ -0,0 +1,246 @@ +import { + load, + ShieldLabsError, + type IdentifyOptions, + type IdentifyResult, + type LoadOptions, + type ShieldLabsAgent, +} from '@shieldlabs-ai/js'; +import { asShieldLabsError, DEFAULT_TIMEOUT, isObject, isValidTimeout, withTimeout } from './internal'; + +/** Where the agent is: `'loading'`, `'ready'`, or `'error'` when it could not be loaded. */ +export type ShieldLabsStatus = 'loading' | 'ready' | 'error'; + +/** The status of the agent for one set of load options (`key`). */ +export interface LoadState { + readonly key: string; + readonly status: ShieldLabsStatus; + readonly error: ShieldLabsError | null; +} + +/** + * The agent of one provider. Plain JavaScript without React: it never runs during render, only when + * an effect or an event handler calls it, so nothing touches the browser during server-side rendering. + */ +export interface AgentLoader { + /** + * Sets the load options. Starts loading when they differ from the current load and loading is + * allowed: from now on with `autoLoad`, otherwise once `load()` has been called. + */ + update(options: LoadOptions, autoLoad: boolean): void; + /** + * Receives the status of the latest load until the returned function is called, starting with the + * last status it reported, if any. + */ + subscribe(listener: (state: LoadState) => void): () => void; + /** Whether an `identify()` or `check()` for this User HID (`null`: anonymous) has not settled yet. */ + isRunning(userId: string | null): boolean; + /** The timeout of a call without one of its own: the provider timeout, else the default of 10000 ms. */ + defaultTimeout: () => number; + /** Allows loading from now on and starts it, or loads again after a failure a new load can fix. */ + load: () => void; + /** The loaded agent. Waits while it loads, and until loading is allowed. */ + getAgent: () => Promise; + /** Rejects with `not_initialized` at once while loading is not allowed. */ + identify: (options?: IdentifyOptions) => Promise; + /** Resolves `null` at once while loading is not allowed. */ + check: (options?: IdentifyOptions) => Promise; +} + +/** One load of the agent for one set of options. */ +interface AgentSlot { + readonly key: string; + readonly promise: Promise; + agent: ShieldLabsAgent | null; + error: ShieldLabsError | null; +} + +const LOAD_FAILED = 'Could not load the ShieldLabs agent.'; +const NOT_LOADED = + 'The ShieldLabs agent is not loaded: ShieldLabsProvider has autoLoad={false} and load() from useShieldLabs() has not been called.'; + +/** Identifies one set of load options. The agent is loaded again when it changes. */ +export function keyOf(options: LoadOptions): string { + return [options.publicKey, options.environment, options.scriptUrl, options.timeout] + .map((value) => typeof value + ':' + String(value)) + .join('|'); +} + +/** The User HID that call options pass, `null` for an anonymous call. */ +export function userIdOf(options: unknown): string | null { + return isObject(options) && typeof options.userId === 'string' ? options.userId : null; +} + +/** Calls `load()` of `@shieldlabs-ai/js`, which is memoized per agent URL and Public Key. */ +function startLoad(options: LoadOptions): Promise { + return new Promise((resolve) => { + resolve(load(options)); + }).catch((error: unknown) => { + throw asShieldLabsError(error, 'load_failed', LOAD_FAILED); + }); +} + +/** + * A failed import is imported again by the next `load()`, and a load that timed out keeps running + * so the next `load()` can use it. Invalid options and unsupported pages stay as they are. + */ +function canRetry(slot: AgentSlot): boolean { + const code = slot.error?.code; + return code === 'load_failed' || code === 'timeout'; +} + +/** + * A load that fails for a reason loading again cannot fix is a setup problem (a missing or wrong + * `publicKey`, a page without HTTPS). The provider only shows it in `status`, so it also goes to the + * console. Blocked or failed downloads are normal for some visitors and stay quiet. + */ +function warnSetupProblem(error: ShieldLabsError): void { + if (error.code === 'invalid_options' || error.code === 'unsupported_environment') { + console.warn('[ShieldLabs] ShieldLabsProvider could not load the agent: ' + error.message); + } +} + +/** How long one call may take in all: its own timeout, the provider timeout or the default. */ +function callTimeout(callOptions: IdentifyOptions | undefined, loadOptions: LoadOptions): number { + const perCall = callOptions?.timeout; + if (isValidTimeout(perCall)) return perCall; + if (isValidTimeout(loadOptions.timeout)) return loadOptions.timeout; + return DEFAULT_TIMEOUT; +} + +/** + * The options for an agent call that waited for the load since `startedAt`: what is left of the + * call's `total` timeout becomes its timeout, so the wait and the agent's answer together stay within + * one timeout (clamped, in case the clock changes). Options that are not an object and an invalid + * timeout go to the agent unchanged, and `@shieldlabs-ai/js` rejects them. + */ +function withTimeLeft( + callOptions: IdentifyOptions | undefined, + total: number, + startedAt: number, +): IdentifyOptions | undefined { + const raw: unknown = callOptions; + if (raw != null && !isObject(raw)) return callOptions; + if (callOptions?.timeout !== undefined && !isValidTimeout(callOptions.timeout)) return callOptions; + const left = total - (Date.now() - startedAt); + return { ...callOptions, timeout: Math.min(total, Math.max(1, left)) }; +} + +export function createAgentLoader(initialOptions: LoadOptions, autoLoad: boolean): AgentLoader { + let options = initialOptions; + let slot: AgentSlot | null = null; + let listener: ((state: LoadState) => void) | null = null; + // The last state the current load reported, kept for a listener that subscribes later. + let latest: LoadState | null = null; + // Whether the agent may load: from the start with autoLoad, otherwise from the first load(). + let allowed = autoLoad; + // Resolves when loading becomes allowed, for the getAgent() calls made before. + let allowedPromise: Promise | null = null; + let resolveAllowed: (() => void) | null = null; + // Unsettled identify() and check() calls per User HID (null: anonymous). + const running = new Map(); + + // The load for the current options: the running or finished one, or a new one. + const currentSlot = (): AgentSlot => { + const key = keyOf(options); + const previous = slot; + if (previous?.key === key && !canRetry(previous)) return previous; + + const next: AgentSlot = { key, promise: startLoad(options), agent: null, error: null }; + slot = next; + latest = null; + // Only the latest load reports. Without a listener (the provider is unmounted, or its effects are + // disconnected inside a hidden ) the state waits in `latest`. + const publish = (state: LoadState): void => { + if (slot !== next) return; + latest = state; + if (listener) listener(state); + }; + if (previous?.key === key) publish({ key, status: 'loading', error: null }); + next.promise.then( + (agent) => { + next.agent = agent; + publish({ key, status: 'ready', error: null }); + }, + (error: unknown) => { + next.error = asShieldLabsError(error, 'load_failed', LOAD_FAILED); + if (slot === next) warnSetupProblem(next.error); + publish({ key, status: 'error', error: next.error }); + }, + ); + return next; + }; + + const allow = (): void => { + if (allowed) return; + allowed = true; + if (resolveAllowed) resolveAllowed(); + allowedPromise = null; + resolveAllowed = null; + }; + + const whenAllowed = (): Promise => + (allowedPromise ??= new Promise((resolve) => { + resolveAllowed = resolve; + })); + + // Calls the agent at once when it has loaded, with the options as they are. Otherwise the call waits + // for the load and then passes the agent only the rest of its timeout: one deadline for the whole call. + const withAgent = ( + callOptions: IdentifyOptions | undefined, + call: (agent: ShieldLabsAgent, agentOptions: IdentifyOptions | undefined) => Promise, + ): Promise => { + const current = currentSlot(); + if (current.agent) return Promise.resolve(current.agent).then((agent) => call(agent, callOptions)); + const ms = callTimeout(callOptions, options); + const startedAt = Date.now(); + return withTimeout(current.promise, ms, 'The ShieldLabs agent did not load within ' + String(ms) + ' ms.').then( + (agent) => call(agent, withTimeLeft(callOptions, ms, startedAt)), + ); + }; + + // Counts the call as running for its User HID until it settles, including a wait for the load. + const track = (callOptions: IdentifyOptions | undefined, promise: Promise): Promise => { + const userId = userIdOf(callOptions); + running.set(userId, (running.get(userId) ?? 0) + 1); + return promise.finally(() => { + const left = (running.get(userId) ?? 1) - 1; + if (left > 0) running.set(userId, left); + else running.delete(userId); + }); + }; + + return { + update(nextOptions, nextAutoLoad) { + options = nextOptions; + if (nextAutoLoad) allow(); + if (allowed) currentSlot(); + }, + subscribe(nextListener) { + listener = nextListener; + // A state reported while nobody listened arrives now. The provider already holds the same + // object when it did listen, so React skips that update. + if (latest) nextListener(latest); + return () => { + if (listener === nextListener) listener = null; + }; + }, + isRunning(userId) { + return running.has(userId); + }, + defaultTimeout: () => callTimeout(undefined, options), + load: () => { + allow(); + currentSlot(); + }, + getAgent: () => (allowed ? currentSlot().promise : whenAllowed().then(() => currentSlot().promise)), + identify: (callOptions) => + allowed + ? track(callOptions, withAgent(callOptions, (agent, agentOptions) => agent.identify(agentOptions))) + : Promise.reject(new ShieldLabsError('not_initialized', NOT_LOADED)), + check: (callOptions) => + allowed + ? track(callOptions, withAgent(callOptions, (agent, agentOptions) => agent.check(agentOptions))) + : Promise.resolve(null), + }; +} diff --git a/src/provider.ts b/src/provider.ts new file mode 100644 index 0000000..2f91d62 --- /dev/null +++ b/src/provider.ts @@ -0,0 +1,194 @@ +import { + createContext, + createElement, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactElement, + type ReactNode, +} from 'react'; +import { + ShieldLabsError, + type IdentifyOptions, + type IdentifyResult, + type LoadOptions, + type ShieldLabsAgent, +} from '@shieldlabs-ai/js'; +import { ignore, isObject } from './internal'; +import { createAgentLoader, keyOf, userIdOf, type LoadState, type ShieldLabsStatus } from './loader'; + +export type { ShieldLabsStatus } from './loader'; + +/** + * Props of {@link ShieldLabsProvider}: the `load()` options of `@shieldlabs-ai/js` plus `autoLoad` and + * `checkOnLoad`. + */ +export interface ShieldLabsProviderProps extends LoadOptions { + /** + * Loads the agent after the first render in the browser. With `false`, nothing loads until `load()` + * from `useShieldLabs()` is called or `autoLoad` becomes `true`, for example once the user has given + * consent. Default `true`. + */ + autoLoad?: boolean; + /** + * Runs `check()` once per provider mount when the agent is ready, for passive monitoring of the + * visit, unless an `identify()` or `check()` for the same User HID is running at that moment. `true` + * checks anonymously, `{ userId }` passes a User HID. Default `false`. + */ + checkOnLoad?: boolean | { userId?: string }; + children?: ReactNode; +} + +/** What {@link useShieldLabs} returns. */ +export interface UseShieldLabsResult { + /** `'loading'` until the agent has loaded, then `'ready'`, or `'error'` when loading failed. */ + status: ShieldLabsStatus; + /** Why loading failed while `status` is `'error'`, otherwise `null`. */ + error: ShieldLabsError | null; + /** + * Runs a fresh identification: a new request ID on every call. Waits for the agent while it is + * loading; the call's timeout covers that wait and the agent's answer. Rejects with a + * `ShieldLabsError` when there is no identification, with `not_initialized` at once while + * `autoLoad={false}` and `load()` has not been called. For protected actions, `useIdentify()` adds + * state and never rejects. + */ + identify: (options?: IdentifyOptions) => Promise; + /** + * Background check, limited by the agent to one per visit every five minutes. Resolves `null` when + * the agent skipped it, and at once while `autoLoad={false}` and `load()` has not been called. Waits + * for the agent while it is loading, within the call's timeout. + */ + check: (options?: IdentifyOptions) => Promise; + /** + * Starts loading the agent: needed only with `autoLoad={false}`, for example once the user has given + * consent. Also loads again after a failed load. Safe to call more than once. Call it from an event + * handler or an effect, never during render. + */ + load: () => void; + /** + * The loaded agent of `@shieldlabs-ai/js`, for example for `agent.identifyOnInteraction(form)`. Waits + * while the agent loads, and with `autoLoad={false}` until loading starts, with no timeout of its + * own. Rejects with the load error when the agent cannot load; the next call loads again when that + * can help. + */ + getAgent: () => Promise; +} + +/** What the provider gives the hooks below it. */ +export interface ShieldLabsContextValue { + /** What `useShieldLabs()` returns. */ + readonly shieldLabs: UseShieldLabsResult; + /** + * The timeout of a call without one of its own (the provider `timeout`, else 10000 ms), read when a + * call starts. `useIdentify()` shares calls on the timeout they run with. + */ + readonly defaultTimeout: () => number; +} + +const ShieldLabsContext = createContext(null); +ShieldLabsContext.displayName = 'ShieldLabsContext'; + +function loadOptionsOf( + publicKey: string, + environment: LoadOptions['environment'], + scriptUrl: string | undefined, + timeout: number | undefined, +): LoadOptions { + const options: LoadOptions = { publicKey }; + if (environment !== undefined) options.environment = environment; + if (scriptUrl !== undefined) options.scriptUrl = scriptUrl; + if (timeout !== undefined) options.timeout = timeout; + return options; +} + +function warnCheckOnLoad(error: unknown): void { + // A background check that is skipped or times out is normal. Invalid options are a mistake to fix. + if (error instanceof ShieldLabsError && error.code === 'invalid_options') { + console.warn('[ShieldLabs] checkOnLoad: ' + error.message); + } +} + +/** + * Loads the ShieldLabs agent once, after the first render in the browser (or once `load()` is called + * with `autoLoad={false}`), and gives the components below it `useShieldLabs()` and `useIdentify()`. + * Renders only its children. Nothing runs during server-side rendering. + */ +export function ShieldLabsProvider(props: ShieldLabsProviderProps): ReactElement { + const { publicKey, environment, scriptUrl, timeout, autoLoad, checkOnLoad = false, children } = props; + const loadsByItself = autoLoad !== false; + const key = keyOf(loadOptionsOf(publicKey, environment, scriptUrl, timeout)); + + // Created once per provider. Creating it touches nothing: the agent loads in the effect below. + const [loader] = useState(() => + createAgentLoader(loadOptionsOf(publicKey, environment, scriptUrl, timeout), loadsByItself), + ); + const [loadState, setLoadState] = useState(() => ({ key, status: 'loading', error: null })); + const checkedRef = useRef(false); + + useEffect(() => { + const unsubscribe = loader.subscribe(setLoadState); + loader.update(loadOptionsOf(publicKey, environment, scriptUrl, timeout), loadsByItself); + // No state updates after unmount: the loader stops reporting to this provider. + return unsubscribe; + }, [loader, publicKey, environment, scriptUrl, timeout, loadsByItself]); + + // A state of older options means the agent for the current options is still loading. + const current = loadState.key === key ? loadState : null; + const status: ShieldLabsStatus = current ? current.status : 'loading'; + const error = current ? current.error : null; + + const checkEnabled = checkOnLoad === true || isObject(checkOnLoad); + const checkUserId = isObject(checkOnLoad) ? checkOnLoad.userId : undefined; + + useEffect(() => { + if (status !== 'ready' || !checkEnabled || checkedRef.current) return; + checkedRef.current = true; + const checkOptions: IdentifyOptions = checkUserId === undefined ? {} : { userId: checkUserId }; + // A call for the same User HID that is still running already identifies the visit, and the agent + // runs one identification at a time per User HID. + if (loader.isRunning(userIdOf(checkOptions))) return; + loader.check(checkOptions).then(ignore, warnCheckOnLoad); + }, [loader, status, checkEnabled, checkUserId]); + + const shieldLabs = useMemo( + () => ({ + status, + error, + identify: loader.identify, + check: loader.check, + load: loader.load, + getAgent: loader.getAgent, + }), + [loader, status, error], + ); + const value = useMemo( + () => ({ shieldLabs, defaultTimeout: loader.defaultTimeout }), + [loader, shieldLabs], + ); + + return createElement(ShieldLabsContext.Provider, { value }, children); +} + +/** The provider value, or an error that names the calling hook when there is no provider. */ +export function useShieldLabsContext(hook: string): ShieldLabsContextValue { + const value = useContext(ShieldLabsContext); + if (value === null) { + throw new Error( + '[ShieldLabs] ' + + hook + + '() must be called inside . Render above the component that calls it.', + ); + } + return value; +} + +/** + * The status of the ShieldLabs agent in the closest {@link ShieldLabsProvider}, its `identify()` and + * `check()`, `load()` for deferred loading and `getAgent()`. + * Throws when there is no provider above the component. + */ +export function useShieldLabs(): UseShieldLabsResult { + return useShieldLabsContext('useShieldLabs').shieldLabs; +} diff --git a/src/use-identify.ts b/src/use-identify.ts new file mode 100644 index 0000000..4019593 --- /dev/null +++ b/src/use-identify.ts @@ -0,0 +1,206 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import type { IdentifyOptions, IdentifyResult, ShieldLabsError } from '@shieldlabs-ai/js'; +import { asShieldLabsError, isObject } from './internal'; +import { useShieldLabsContext } from './provider'; + +/** Options of {@link useIdentify}. */ +export interface UseIdentifyOptions { + /** + * User HID for every identification of this hook: a hashed or pseudonymous account ID computed on + * your server. Omit it for anonymous visitors. Options of `identify()` with a `userId` key override + * it, also when the value is `undefined` or `null`, which both identify anonymously. + */ + userId?: string; + /** + * Runs `identify()` once when the component mounts, as soon as the agent is ready. With + * `autoLoad={false}`, a mount before `load()` ends at once with a `not_initialized` error and does + * not run again after `load()`. Every run is a billable identification, so use it sparingly. + * Default `false`. + */ + runOnMount?: boolean; +} + +/** What {@link useIdentify} returns. */ +export interface UseIdentifyResult { + /** + * Runs a fresh identification (a new request ID on every call) and resolves its result, or `null` + * when there is no identification: the reason is then in `error`. Never rejects. While a call of + * this hook with the same User HID and `timeout` is running (a double submit), returns that call + * instead of starting another identification. The User HID of a call is its own `userId` when the + * options have that key (`undefined` and `null` both mean anonymous), else the hook's. A call + * without `timeout` counts as one with the provider `timeout` (10000 ms by default). + */ + identify: (options?: IdentifyOptions) => Promise; + /** The result of the latest identification, `null` before it resolves, when it failed or after `reset()`. */ + result: IdentifyResult | null; + /** `true` while the latest identification is running. */ + isLoading: boolean; + /** Why the latest identification failed, otherwise `null`. */ + error: ShieldLabsError | null; + /** Clears `result` and `error`. A running identification no longer updates the state. */ + reset: () => void; +} + +interface IdentifyState { + readonly result: IdentifyResult | null; + readonly isLoading: boolean; + readonly error: ShieldLabsError | null; +} + +/** An identification that has not settled yet, shared with calls for the same User HID and timeout. */ +interface RunningCall { + readonly userId: string | undefined; + /** The timeout the call runs with: its own `timeout` (even an invalid one), else the provider's default. */ + readonly timeout: unknown; + readonly promise: Promise; + /** Identifies the call. The state shows the outcome of the call whose token is in `latestRef`. */ + readonly token: object; +} + +/** + * The call options with the effective User HID: the call's own when its options have a `userId` + * key, even one set to `undefined` or `null` (both identify anonymously), else the hook's. An + * anonymous call carries no `userId`, so it goes to the agent and is shared like any other. + */ +function agentOptionsOf(callOptions: IdentifyOptions | undefined, userId: string | undefined): IdentifyOptions | undefined { + // Anything but an options object goes to the agent unchanged, so that @shieldlabs-ai/js reports it. + const raw: unknown = callOptions; + if (raw != null && !isObject(raw)) return callOptions; + const { userId: ownUserId, ...rest } = callOptions ?? {}; + const effectiveUserId = callOptions != null && 'userId' in callOptions ? ownUserId : userId; + return effectiveUserId == null ? rest : { ...rest, userId: effectiveUserId }; +} + +/** + * The timeout a call with `options` runs with: its own, else `defaultTimeout`. As in `@shieldlabs-ai/js`, + * only an omitted timeout takes the default. Any other value stays as it is, so a timeout the agent + * rejects is never shared with a call that can succeed. + */ +function timeoutOf(options: IdentifyOptions | undefined, defaultTimeout: number): unknown { + // Not `??`: a null timeout is invalid, and `??` would give it the default. + const timeout: unknown = options?.timeout; + return timeout === undefined ? defaultTimeout : timeout; +} + +/** Whether a call with `options` that runs with `timeout` asks for the same identification as `running`. */ +function isSameCall(running: RunningCall, options: IdentifyOptions | undefined, timeout: unknown): boolean { + const raw: unknown = options; + return isObject(raw) && running.userId === options?.userId && running.timeout === timeout; +} + +// Shared objects, so that setting the same state again does not render again. +const IDLE: IdentifyState = { result: null, isLoading: false, error: null }; +const LOADING: IdentifyState = { result: null, isLoading: true, error: null }; + +/** + * Identification with loading and error state, for protected actions such as signup, login or + * checkout. Must be called inside a {@link ShieldLabsProvider}. + */ +export function useIdentify(options: UseIdentifyOptions = {}): UseIdentifyResult { + const { shieldLabs, defaultTimeout } = useShieldLabsContext('useIdentify'); + const identifyWithAgent = shieldLabs.identify; + const { userId, runOnMount = false } = options; + + const [state, setState] = useState(runOnMount ? LOADING : IDLE); + // True between the mount effect and its cleanup. The effects also disconnect while the component is + // inside a hidden , which keeps the state: an update that arrives then waits in queuedRef + // until the component is shown again, and one that arrives after an unmount is dropped. + const connectedRef = useRef(false); + const queuedRef = useRef(null); + // The token of the call whose outcome the state shows: the call asked for last. + const latestRef = useRef({}); + const runningRef = useRef([]); + // Consumed by the first effect run, so identify() runs once per mount (also in StrictMode). + const mountRunRef = useRef(runOnMount); + + const apply = useCallback((next: IdentifyState): void => { + if (connectedRef.current) setState(next); + else queuedRef.current = next; + }, []); + + useEffect(() => { + connectedRef.current = true; + const queued = queuedRef.current; + queuedRef.current = null; + if (queued) apply(queued); + return () => { + connectedRef.current = false; + }; + }, [apply]); + + const identify = useCallback( + (callOptions?: IdentifyOptions): Promise => { + const agentOptions = agentOptionsOf(callOptions, userId); + // Read now: the provider timeout can change between calls. + const timeout = timeoutOf(agentOptions, defaultTimeout()); + // A double submit gets the running identification instead of a second one, which the agent + // would refuse while the first one runs. + const shared = runningRef.current.find((running) => isSameCall(running, agentOptions, timeout)); + if (shared) { + if (latestRef.current !== shared.token) { + latestRef.current = shared.token; + apply(LOADING); + } + return shared.promise; + } + + const token = {}; + latestRef.current = token; + // Only the call asked for last changes the state. + const publish = (next: IdentifyState): void => { + if (latestRef.current === token) apply(next); + }; + let entry: RunningCall | null = null; + const settle = (next: IdentifyState): void => { + runningRef.current = runningRef.current.filter((running) => running !== entry); + publish(next); + }; + + publish(LOADING); + // Started inside the promise, so that even a synchronous throw ends up in `error`. + const promise = new Promise((resolve) => { + resolve(identifyWithAgent(agentOptions)); + }).then( + (result) => { + settle({ result, isLoading: false, error: null }); + return result; + }, + (error: unknown) => { + // No rejection: the protected action goes ahead without a request ID, as unverified. + settle({ + result: null, + isLoading: false, + error: asShieldLabsError(error, 'not_initialized', 'The identification did not complete.'), + }); + return null; + }, + ); + + // Options that are not an object go to the agent, which rejects them: nothing to share. + const raw: unknown = agentOptions; + if (isObject(raw)) { + entry = { userId: agentOptions?.userId, timeout, promise, token }; + runningRef.current = [...runningRef.current, entry]; + } + return promise; + }, + [identifyWithAgent, defaultTimeout, userId, apply], + ); + + const reset = useCallback((): void => { + latestRef.current = {}; + runningRef.current = []; + apply(IDLE); + }, [apply]); + + useEffect(() => { + if (!mountRunRef.current) return; + mountRunRef.current = false; + void identify(); + }, [identify]); + + return useMemo( + () => ({ identify, result: state.result, isLoading: state.isLoading, error: state.error, reset }), + [identify, state, reset], + ); +} diff --git a/test/activity.test.tsx b/test/activity.test.tsx new file mode 100644 index 0000000..9130a1d --- /dev/null +++ b/test/activity.test.tsx @@ -0,0 +1,196 @@ +// (React 19.2 and later) keeps the state of its children but disconnects their +// effects until it is visible again. Agent loads and identifications that settle meanwhile must show up +// when the content is visible again. Skipped on React versions without . +import * as React from 'react'; +import type { ComponentType, ReactNode } from 'react'; +import { act, render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, ShieldLabsError, type IdentifyResult, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { ShieldLabsProvider, useIdentify, useShieldLabs, type ShieldLabsProviderProps } from '../src'; +import { createAgent, deferred, PUBLIC_KEY, USER_HID } from './support/agent'; +import { flush } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +type ActivityComponent = ComponentType<{ mode: 'visible' | 'hidden'; children?: ReactNode }>; +const Activity = (React as unknown as { Activity?: ActivityComponent }).Activity; + +const REQUEST_ID = '7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d'; + +interface InActivity { + /** The value the hook returned in the latest render. */ + readonly value: T; + hide(): void; + show(): void; +} + +/** + * Renders a component that calls `useValue()` inside a provider, with an boundary that the + * test hides and shows: around the provider, or between the provider and the component. + */ +function renderInActivity( + useValue: () => T, + boundary: 'around provider' | 'around component', + props: Partial = {}, +): InActivity { + if (!Activity) throw new Error(' needs React 19.2 or later.'); + const Boundary = Activity; + const seen: { value?: T } = {}; + function Consumer(): null { + seen.value = useValue(); + return null; + } + const tree = (hidden: boolean) => { + const mode = hidden ? 'hidden' : 'visible'; + return boundary === 'around provider' ? ( + + + + + + ) : ( + + + + + + ); + }; + const utils = render(tree(false)); + return { + get value(): T { + if (seen.value === undefined) throw new Error('The component has not rendered.'); + return seen.value; + }, + hide: () => { + utils.rerender(tree(true)); + }, + show: () => { + utils.rerender(tree(false)); + }, + }; +} + +describe.skipIf(!Activity)('inside a hidden ', () => { + it('the provider becomes ready and runs checkOnLoad once when the agent loaded while it was hidden', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, check } = createAgent(); + const view = renderInActivity(() => useShieldLabs(), 'around provider', { checkOnLoad: true }); + await flush(); + expect(view.value.status).toBe('loading'); + + view.hide(); + pending.resolve(agent); + await flush(); + expect(check).not.toHaveBeenCalled(); + + view.show(); + await flush(); + expect(view.value.status).toBe('ready'); + expect(check).toHaveBeenCalledTimes(1); + + view.hide(); + view.show(); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('the provider shows a load error that happened while it was hidden', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const view = renderInActivity(() => useShieldLabs(), 'around provider'); + view.hide(); + // An error that loading again does not fix, so showing the provider does not start a new load. + const failure = new ShieldLabsError('unsupported_environment', 'The page is not a secure context.'); + pending.reject(failure); + await flush(); + + view.show(); + await flush(); + expect(view.value).toMatchObject({ status: 'error', error: failure }); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('useIdentify() shows a result that arrived while the component was hidden', async () => { + const { agent, identify } = createAgent(); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + loadMock.mockResolvedValue(agent); + const view = renderInActivity(() => useIdentify({ runOnMount: true, userId: USER_HID }), 'around component'); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + expect(view.value.isLoading).toBe(true); + + view.hide(); + running.resolve({ requestId: REQUEST_ID, userId: USER_HID }); + await flush(); + + view.show(); + await flush(); + expect(view.value).toMatchObject({ result: { requestId: REQUEST_ID, userId: USER_HID }, isLoading: false, error: null }); + // Showing the component again is not a new mount: runOnMount does not identify again. + expect(identify).toHaveBeenCalledTimes(1); + }); + + it('useIdentify() shows a failure that arrived while the component was hidden', async () => { + const { agent, identify } = createAgent(); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + loadMock.mockResolvedValue(agent); + const view = renderInActivity(() => useIdentify(), 'around component'); + await flush(); + let identified: Promise | undefined; + act(() => { + identified = view.value.identify(); + }); + await flush(); + + view.hide(); + const failure = new ShieldLabsError('not_initialized', 'The agent did not start an identification.'); + running.reject(failure); + await expect(identified).resolves.toBeNull(); + await flush(); + + view.show(); + await flush(); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: failure }); + }); + + it('useIdentify() applies reset() and identify() called while the component was hidden', async () => { + const { agent, identify } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInActivity(() => useIdentify(), 'around component'); + await flush(); + await act(() => view.value.identify()); + expect(view.value.result).not.toBeNull(); + + view.hide(); + act(() => { + view.value.reset(); + }); + view.show(); + await flush(); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: null }); + + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + view.hide(); + act(() => { + void view.value.identify(); + }); + view.show(); + expect(view.value).toMatchObject({ result: null, isLoading: true, error: null }); + + running.resolve({ requestId: REQUEST_ID, userId: null }); + await flush(); + expect(view.value).toMatchObject({ result: { requestId: REQUEST_ID }, isLoading: false }); + }); +}); diff --git a/test/auto-load.test.tsx b/test/auto-load.test.tsx new file mode 100644 index 0000000..a2e4f1e --- /dev/null +++ b/test/auto-load.test.tsx @@ -0,0 +1,214 @@ +import { act } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, ShieldLabsError, type IdentifyResult, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { useIdentify, useShieldLabs } from '../src'; +import { createAgent, deferred, OTHER_PUBLIC_KEY, PUBLIC_KEY, USER_HID } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +describe('autoLoad={false}', () => { + it.each([false, true])('loads nothing until load() is called, then loads once (StrictMode: %s)', async (strict) => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false, timeout: 5000 }, { strict }); + await flush(); + expect(loadMock).not.toHaveBeenCalled(); + expect(view.value).toMatchObject({ status: 'loading', error: null }); + + // load() returns nothing: there is no promise to await. + const loadAgent: () => unknown = view.value.load; + let returned: unknown = 'not called'; + act(() => { + returned = loadAgent(); + }); + expect(returned).toBeUndefined(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledWith({ publicKey: PUBLIC_KEY, timeout: 5000 }); + + pending.resolve(createAgent().agent); + await flush(); + expect(view.value.status).toBe('ready'); + act(() => { + view.value.load(); + }); + view.rerender({ autoLoad: false, timeout: 5000 }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('loads when autoLoad becomes true', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + await flush(); + expect(loadMock).not.toHaveBeenCalled(); + + view.rerender({ autoLoad: true }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(view.value.status).toBe('ready'); + }); + + it('keeps the agent when autoLoad goes back to false', async () => { + const { agent, identify } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + view.rerender({ autoLoad: false }); + await flush(); + expect(view.value.status).toBe('ready'); + await act(() => view.value.identify()); + expect(identify).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('loads with the options of the latest render when load() is called', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + view.rerender({ autoLoad: false, publicKey: OTHER_PUBLIC_KEY }); + await flush(); + expect(loadMock).not.toHaveBeenCalled(); + + act(() => { + view.value.load(); + }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledWith({ publicKey: OTHER_PUBLIC_KEY }); + expect(view.value.status).toBe('ready'); + }); + + it('loads again for new options once load() has been called', async () => { + loadMock.mockImplementation(() => Promise.resolve(createAgent().agent)); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + act(() => { + view.value.load(); + }); + await flush(); + view.rerender({ autoLoad: false, publicKey: OTHER_PUBLIC_KEY }); + expect(view.value.status).toBe('loading'); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(2); + expect(loadMock).toHaveBeenLastCalledWith({ publicKey: OTHER_PUBLIC_KEY }); + expect(view.value.status).toBe('ready'); + }); + + it('answers calls made before load() at once and loads nothing: identify() rejects, check() resolves null', async () => { + const { agent, identify, check } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + await flush(); + + await expect(view.value.identify({ userId: USER_HID })).rejects.toMatchObject({ + name: 'ShieldLabsError', + code: 'not_initialized', + message: expect.stringContaining('autoLoad={false}') as string, + }); + await expect(view.value.check()).resolves.toBeNull(); + expect(loadMock).not.toHaveBeenCalled(); + expect(identify).not.toHaveBeenCalled(); + expect(check).not.toHaveBeenCalled(); + expect(view.value.status).toBe('loading'); + }); + + it('useIdentify().identify() resolves null with a not_initialized error before load(), then identifies after it', async () => { + const { agent, identify } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider( + () => ({ shieldLabs: useShieldLabs(), helper: useIdentify({ userId: USER_HID }) }), + { autoLoad: false }, + ); + + let outcome: IdentifyResult | null | undefined; + await act(async () => { + outcome = await view.value.helper.identify(); + }); + expect(outcome).toBeNull(); + expect(view.value.helper.error).toBeInstanceOf(ShieldLabsError); + expect(view.value.helper).toMatchObject({ result: null, isLoading: false, error: { code: 'not_initialized' } }); + expect(loadMock).not.toHaveBeenCalled(); + + act(() => { + view.value.shieldLabs.load(); + }); + await act(async () => { + outcome = await view.value.helper.identify(); + }); + expect(outcome).toMatchObject({ userId: USER_HID }); + expect(identify).toHaveBeenCalledTimes(1); + expect(view.value.helper).toMatchObject({ result: outcome, isLoading: false, error: null }); + }); + + it('runOnMount before load() ends with a not_initialized error instead of waiting, and does not run again after load()', async () => { + const { agent, identify } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider( + () => ({ shieldLabs: useShieldLabs(), helper: useIdentify({ runOnMount: true }) }), + { autoLoad: false }, + ); + expect(view.value.helper.isLoading).toBe(true); + await flush(); + expect(view.value.helper).toMatchObject({ result: null, isLoading: false, error: { code: 'not_initialized' } }); + expect(loadMock).not.toHaveBeenCalled(); + + act(() => { + view.value.shieldLabs.load(); + }); + await flush(); + expect(view.value.shieldLabs.status).toBe('ready'); + expect(identify).not.toHaveBeenCalled(); + expect(view.value.helper).toMatchObject({ result: null, isLoading: false, error: { code: 'not_initialized' } }); + }); + + it('runs checkOnLoad once the agent is ready after load()', async () => { + const { agent, check } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false, checkOnLoad: { userId: USER_HID } }); + await flush(); + expect(check).not.toHaveBeenCalled(); + + act(() => { + view.value.load(); + }); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledWith({ userId: USER_HID }); + }); +}); + +describe('useShieldLabs().load()', () => { + it('loads again after a failed load, and the status follows', async () => { + const { agent } = createAgent(); + loadMock.mockRejectedValueOnce(new ShieldLabsError('load_failed', 'Network error.')).mockResolvedValueOnce(agent); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + expect(view.value.status).toBe('error'); + + act(() => { + view.value.load(); + }); + expect(view.value).toMatchObject({ status: 'loading', error: null }); + await flush(); + expect(view.value.status).toBe('ready'); + expect(loadMock).toHaveBeenCalledTimes(2); + }); + + it('does not load again after invalid options', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const failure = new ShieldLabsError('invalid_options', 'publicKey must match ^[A-Za-z0-9_-]{1,128}$.'); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + act(() => { + view.value.load(); + }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(view.value).toMatchObject({ status: 'error', error: failure }); + }); +}); diff --git a/test/check-on-load.test.tsx b/test/check-on-load.test.tsx new file mode 100644 index 0000000..6fe51ea --- /dev/null +++ b/test/check-on-load.test.tsx @@ -0,0 +1,207 @@ +import { act } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, ShieldLabsError, type IdentifyResult, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { useIdentify, useShieldLabs, type ShieldLabsProviderProps } from '../src'; +import { createAgent, deferred, OTHER_PUBLIC_KEY, OTHER_USER_HID, USER_HID } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +describe('checkOnLoad', () => { + it('runs one anonymous check() when the agent is ready with checkOnLoad', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, check, identify } = createAgent(); + renderInProvider(() => useShieldLabs(), { checkOnLoad: true }); + await flush(); + expect(check).not.toHaveBeenCalled(); + + pending.resolve(agent); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledWith({}); + expect(identify).not.toHaveBeenCalled(); + }); + + it('passes the User HID of checkOnLoad={{ userId }}', async () => { + const { agent, check } = createAgent(); + loadMock.mockResolvedValue(agent); + renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: USER_HID } }); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledWith({ userId: USER_HID }); + }); + + it('checks anonymously for checkOnLoad={{}}', async () => { + const { agent, check } = createAgent(); + loadMock.mockResolvedValue(agent); + renderInProvider(() => useShieldLabs(), { checkOnLoad: {} }); + await flush(); + expect(check).toHaveBeenCalledWith({}); + }); + + it.each([{}, { checkOnLoad: false }] satisfies Partial[])( + 'does not check for %o', + async (props) => { + const { agent, check } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), props); + await flush(); + expect(view.value.status).toBe('ready'); + expect(check).not.toHaveBeenCalled(); + }, + ); + + it('runs once per provider mount, whatever changes afterwards', async () => { + const first = createAgent(); + const second = createAgent(); + loadMock.mockResolvedValueOnce(first.agent).mockResolvedValueOnce(second.agent); + const view = renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: USER_HID } }); + await flush(); + expect(first.check).toHaveBeenCalledTimes(1); + + view.rerender({ checkOnLoad: { userId: USER_HID } }); + view.rerender({ checkOnLoad: { userId: OTHER_USER_HID } }); + view.rerender({ checkOnLoad: true, publicKey: OTHER_PUBLIC_KEY }); + await flush(); + expect(view.value.status).toBe('ready'); + expect(loadMock).toHaveBeenCalledTimes(2); + expect(first.check).toHaveBeenCalledTimes(1); + expect(second.check).not.toHaveBeenCalled(); + }); + + it('runs once when it is turned on after the agent is ready', async () => { + const { agent, check } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { checkOnLoad: false }); + await flush(); + expect(check).not.toHaveBeenCalled(); + + view.rerender({ checkOnLoad: { userId: USER_HID } }); + await flush(); + view.rerender({ checkOnLoad: true }); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledWith({ userId: USER_HID }); + }); + + it('runs when a later load succeeds after a failed one', async () => { + const { agent, check } = createAgent(); + loadMock.mockRejectedValueOnce(new ShieldLabsError('load_failed', 'Network error.')).mockResolvedValueOnce(agent); + const view = renderInProvider(() => useShieldLabs(), { checkOnLoad: true }); + await flush(); + expect(view.value.status).toBe('error'); + expect(check).not.toHaveBeenCalled(); + + // The identify() that loads the agent again has settled by the time the check runs. + await act(() => view.value.identify()); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + }); + + it('is skipped when an identify() for the same User HID is running as the agent becomes ready', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, check, identify } = createAgent(); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + // runOnMount waits for the agent, anonymously, like checkOnLoad={true}. + renderInProvider(() => useIdentify({ runOnMount: true }), { checkOnLoad: true }); + await flush(); + + pending.resolve(agent); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + expect(check).not.toHaveBeenCalled(); + + // It stays skipped: checkOnLoad runs at most once per provider mount. + running.resolve({ requestId: 'cf3a4b5c-6d7e-4f8a-9b9c-0d1e2f3a4b5c', userId: null }); + await flush(); + expect(check).not.toHaveBeenCalled(); + }); + + it('is skipped when a check() for the same User HID is running', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, check } = createAgent(); + check.mockReturnValueOnce(new Promise(() => undefined)); + const view = renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: USER_HID } }); + act(() => { + void view.value.check({ userId: USER_HID }); + }); + + pending.resolve(agent); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledWith({ userId: USER_HID, timeout: expect.any(Number) as number }); + }); + + it('runs when the running call is for another User HID', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, check, identify } = createAgent(); + identify.mockReturnValueOnce(new Promise(() => undefined)); + const view = renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: USER_HID } }); + act(() => { + void view.value.identify({ userId: OTHER_USER_HID }); + }); + + pending.resolve(agent); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledWith({ userId: USER_HID }); + }); + + it('runs when an anonymous call is running and checkOnLoad passes a User HID, and the reverse', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, check, identify } = createAgent(); + identify.mockReturnValue(new Promise(() => undefined)); + const anonymousCall = renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: USER_HID } }); + act(() => { + void anonymousCall.value.identify(); + }); + pending.resolve(agent); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + anonymousCall.unmount(); + + loadMock.mockResolvedValue(agent); + const userCall = renderInProvider(() => useShieldLabs(), { checkOnLoad: true }); + act(() => { + void userCall.value.identify({ userId: USER_HID }); + }); + await flush(); + expect(check).toHaveBeenCalledTimes(2); + expect(check).toHaveBeenLastCalledWith({}); + }); + + it('ignores a check that is skipped or fails, and keeps the provider ready', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const { agent, check } = createAgent(); + check.mockRejectedValueOnce(new ShieldLabsError('timeout', 'The agent did not answer.')); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { checkOnLoad: true }); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + expect(view.value).toMatchObject({ status: 'ready', error: null }); + expect(warn).not.toHaveBeenCalled(); + }); + + it('warns when the check options are invalid', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const { agent, check } = createAgent(); + check.mockRejectedValueOnce(new ShieldLabsError('invalid_options', 'userId "anonymous" is reserved.')); + loadMock.mockResolvedValue(agent); + renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: 'anonymous' } }); + await flush(); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith('[ShieldLabs] checkOnLoad: userId "anonymous" is reserved.'); + }); +}); diff --git a/test/dist.test.ts b/test/dist.test.ts new file mode 100644 index 0000000..9d84cf5 --- /dev/null +++ b/test/dist.test.ts @@ -0,0 +1,84 @@ +// @vitest-environment node +// Checks the built files (`npm test` builds first): the "use client" directive, the exports of both +// formats, the declarations, and server rendering with the real @shieldlabs-ai/js. +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { describe, expect, it } from 'vitest'; +import { PUBLIC_KEY } from './support/agent'; + +const root = new URL('../', import.meta.url); +const require = createRequire(import.meta.url); +const EXPORTS = ['ShieldLabsError', 'ShieldLabsProvider', 'useIdentify', 'useShieldLabs']; + +type Api = typeof import('../src'); + +function read(file: string): string { + return readFileSync(new URL(file, root), 'utf8'); +} + +/** Server-renders a provider with a component that uses both hooks, from one build of the package. */ +function renderWith(api: Api): string { + const { createElement } = require('react') as typeof import('react'); + const { renderToString } = require('react-dom/server') as typeof import('react-dom/server'); + function Probe() { + const { status, load: loadAgent, getAgent } = api.useShieldLabs(); + const { isLoading } = api.useIdentify({ runOnMount: true }); + return createElement('span', null, [status, String(isLoading), typeof loadAgent, typeof getAgent].join(' ')); + } + return renderToString(createElement(api.ShieldLabsProvider, { publicKey: PUBLIC_KEY, checkOnLoad: true }, createElement(Probe))); +} + +describe('built package', () => { + it.each(['dist/index.js', 'dist/index.cjs'])('%s starts with the "use client" directive', (file) => { + expect(read(file).startsWith('"use client";\n')).toBe(true); + }); + + it('keeps React and @shieldlabs-ai/js external', () => { + const esm = read('dist/index.js'); + expect(esm).toContain('from "react"'); + expect(esm).toContain('from "@shieldlabs-ai/js"'); + const cjs = read('dist/index.cjs'); + expect(cjs).toContain('require("react")'); + expect(cjs).toContain('require("@shieldlabs-ai/js")'); + }); + + it('the CommonJS build exports the public API and the ShieldLabsError of @shieldlabs-ai/js', () => { + const api = require('../dist/index.cjs') as Api; + expect(Object.keys(api).sort()).toEqual(EXPORTS); + const core = require('@shieldlabs-ai/js') as typeof import('@shieldlabs-ai/js'); + expect(api.ShieldLabsError).toBe(core.ShieldLabsError); + expect(renderWith(api)).toBe('loading true function function'); + }); + + it('the ES module build exports the public API and the ShieldLabsError of @shieldlabs-ai/js', async () => { + // A computed specifier: the type check runs before the build, when dist/ does not exist yet. + const esm = new URL('dist/index.js', root).href; + const api = (await import(/* @vite-ignore */ esm)) as Api; + expect(Object.keys(api).sort()).toEqual(EXPORTS); + const core = await import('@shieldlabs-ai/js'); + expect(api.ShieldLabsError).toBe(core.ShieldLabsError); + }); + + it.each(['dist/index.d.ts', 'dist/index.d.cts'])('%s declares the public API', (file) => { + const types = read(file); + for (const name of [ + 'ShieldLabsProvider', + 'ShieldLabsProviderProps', + 'ShieldLabsStatus', + 'UseShieldLabsResult', + 'useShieldLabs', + 'useIdentify', + 'UseIdentifyOptions', + 'UseIdentifyResult', + 'ShieldLabsError', + 'IdentifyOptions', + 'IdentifyResult', + 'InteractionIdentifier', + 'LoadOptions', + 'ShieldLabsAgent', + 'ShieldLabsErrorCode', + ]) { + expect(types).toMatch(new RegExp('\\b' + name + '\\b')); + } + }); +}); diff --git a/test/get-agent.test.tsx b/test/get-agent.test.tsx new file mode 100644 index 0000000..b846637 --- /dev/null +++ b/test/get-agent.test.tsx @@ -0,0 +1,245 @@ +import { StrictMode, useEffect, useRef, type SyntheticEvent } from 'react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, ShieldLabsError, type InteractionIdentifier, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { ShieldLabsProvider, useShieldLabs, type ShieldLabsProviderProps } from '../src'; +import { createAgent, deferred, PUBLIC_KEY } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +describe('useShieldLabs().getAgent()', () => { + it('resolves the agent that load() of @shieldlabs-ai/js resolved, once it has loaded', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent } = createAgent(); + const view = renderInProvider(() => useShieldLabs()); + + let resolved: ShieldLabsAgent | undefined; + void view.value.getAgent().then((value) => { + resolved = value; + }); + await flush(); + expect(resolved).toBeUndefined(); + + pending.resolve(agent); + await flush(); + expect(resolved).toBe(agent); + await expect(view.value.getAgent()).resolves.toBe(agent); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('rejects with the load error, and loads again on the next call after a failure a new load can fix', async () => { + const { agent } = createAgent(); + const failure = new ShieldLabsError('load_failed', 'Network error.'); + loadMock.mockRejectedValueOnce(failure).mockResolvedValueOnce(agent); + const view = renderInProvider(() => useShieldLabs()); + const first = view.value.getAgent(); + await flush(); + await expect(first).rejects.toBe(failure); + expect(view.value.status).toBe('error'); + + let again: Promise | undefined; + act(() => { + again = view.value.getAgent(); + }); + expect(view.value.status).toBe('loading'); + await flush(); + await expect(again).resolves.toBe(agent); + expect(view.value.status).toBe('ready'); + expect(loadMock).toHaveBeenCalledTimes(2); + }); + + it('rejects with invalid options without loading again', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const failure = new ShieldLabsError('invalid_options', 'publicKey must match ^[A-Za-z0-9_-]{1,128}$.'); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + await expect(view.value.getAgent()).rejects.toBe(failure); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('waits until load() with autoLoad={false}, and loads nothing before', async () => { + const { agent } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + + let resolved: ShieldLabsAgent | undefined; + void view.value.getAgent().then((value) => { + resolved = value; + }); + void view.value.getAgent(); + await flush(); + expect(resolved).toBeUndefined(); + expect(loadMock).not.toHaveBeenCalled(); + + act(() => { + view.value.load(); + }); + await flush(); + expect(resolved).toBe(agent); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('has no timeout of its own while it waits for load() with autoLoad={false}', async () => { + vi.useFakeTimers(); + const { agent } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false, timeout: 3000 }); + + let outcome: 'resolved' | 'rejected' | undefined; + void view.value.getAgent().then( + () => { + outcome = 'resolved'; + }, + () => { + outcome = 'rejected'; + }, + ); + await act(() => vi.advanceTimersByTimeAsync(60_000)); + expect(outcome).toBeUndefined(); + expect(vi.getTimerCount()).toBe(0); + + act(() => { + view.value.load(); + }); + await act(() => vi.advanceTimersByTimeAsync(0)); + expect(outcome).toBe('resolved'); + }); + + it('rejects with the error of the load that load() starts with autoLoad={false}', async () => { + const failure = new ShieldLabsError('load_failed', 'Network error.'); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + let rejection: unknown; + void view.value.getAgent().catch((error: unknown) => { + rejection = error; + }); + await flush(); + expect(loadMock).not.toHaveBeenCalled(); + expect(rejection).toBeUndefined(); + + act(() => { + view.value.load(); + }); + await flush(); + expect(rejection).toBe(failure); + expect(view.value).toMatchObject({ status: 'error', error: failure }); + }); + + it('keeps load and getAgent stable across renders', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs()); + const { load: loadAgent, getAgent } = view.value; + await flush(); + view.rerender({ checkOnLoad: true }); + await flush(); + expect(view.value.load).toBe(loadAgent); + expect(view.value.getAgent).toBe(getAgent); + }); +}); + +/** Early identification before a full-page post, as the README shows it. */ +function SignupForm() { + const { getAgent } = useShieldLabs(); + const formRef = useRef(null); + const handle = useRef(null); + + useEffect(() => { + const form = formRef.current!; + let active = true; + getAgent().then( + (agent) => { + if (active) handle.current = agent.identifyOnInteraction(form); + }, + () => undefined, + ); + return () => { + active = false; + handle.current?.dispose(); + handle.current = null; + }; + }, [getAgent]); + + async function onSubmit(event: SyntheticEvent) { + event.preventDefault(); + const form = event.currentTarget; + const result = await handle.current?.take().catch(() => null); + (form.elements.namedItem('requestId') as HTMLInputElement).value = result?.requestId ?? ''; + } + + return ( +
void onSubmit(event)}> + + +
+ ); +} + +function renderForm(props: Partial = {}, strict = false) { + const tree = ( + + + + ); + return render(strict ? {tree} : tree); +} + +describe('early identification with getAgent() and identifyOnInteraction()', () => { + it.each([false, true])('arms the form once and fills the request ID on submit (StrictMode: %s)', async (strict) => { + const { agent, identifyOnInteraction, handle } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderForm({}, strict); + await flush(); + + const form = screen.getByRole('form', { name: 'signup' }); + expect(identifyOnInteraction).toHaveBeenCalledTimes(1); + expect(identifyOnInteraction).toHaveBeenCalledWith(form); + + fireEvent.submit(form); + await flush(); + expect(handle.take).toHaveBeenCalledTimes(1); + expect(screen.getByTestId('requestId').value).toMatch(/^[0-9a-f-]{36}$/); + + view.unmount(); + expect(handle.dispose).toHaveBeenCalledTimes(1); + }); + + it('submits without a request ID when the agent cannot load', async () => { + loadMock.mockRejectedValue(new ShieldLabsError('load_failed', 'Blocked by the page.')); + renderForm(); + await flush(); + fireEvent.submit(screen.getByRole('form', { name: 'signup' })); + await flush(); + expect(screen.getByTestId('requestId').value).toBe(''); + }); + + it('with autoLoad={false}, arms the form once load() has been called', async () => { + const { agent, identifyOnInteraction } = createAgent(); + loadMock.mockResolvedValue(agent); + function ConsentButton() { + const { load: loadAgent } = useShieldLabs(); + return ; + } + render( + + + + , + ); + await flush(); + expect(loadMock).not.toHaveBeenCalled(); + expect(identifyOnInteraction).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { name: 'Accept' })); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(identifyOnInteraction).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/outside-provider.test.tsx b/test/outside-provider.test.tsx new file mode 100644 index 0000000..ce0e554 --- /dev/null +++ b/test/outside-provider.test.tsx @@ -0,0 +1,30 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load } from '@shieldlabs-ai/js'; +import { useIdentify, useShieldLabs } from '../src'; +import { clearConsoleError, expectConsoleError } from './support/console'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +describe('hooks outside ', () => { + it('useShieldLabs() throws an error that names the hook and the provider', () => { + expectConsoleError(); + expect(() => renderHook(() => useShieldLabs())).toThrow( + '[ShieldLabs] useShieldLabs() must be called inside . Render above the component that calls it.', + ); + expect(load).not.toHaveBeenCalled(); + clearConsoleError(); + }); + + it('useIdentify() throws an error that names the hook', () => { + expectConsoleError(); + expect(() => renderHook(() => useIdentify({ runOnMount: true }))).toThrow( + /^\[ShieldLabs\] useIdentify\(\) must be called inside \./, + ); + expect(load).not.toHaveBeenCalled(); + clearConsoleError(); + }); +}); diff --git a/test/provider.test.tsx b/test/provider.test.tsx new file mode 100644 index 0000000..ecc6024 --- /dev/null +++ b/test/provider.test.tsx @@ -0,0 +1,428 @@ +import { act } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, ShieldLabsError, type IdentifyOptions, type IdentifyResult, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { useShieldLabs } from '../src'; +import { createAgent, deferred, OTHER_PUBLIC_KEY, PUBLIC_KEY, USER_HID } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +describe('ShieldLabsProvider', () => { + it('starts in loading, loads the agent once with its options and becomes ready', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const view = renderInProvider(() => useShieldLabs(), { + environment: 'development', + scriptUrl: 'http://localhost:8080/snippet.js', + timeout: 5000, + }); + + expect(view.value.status).toBe('loading'); + expect(view.value.error).toBeNull(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledWith({ + publicKey: PUBLIC_KEY, + environment: 'development', + scriptUrl: 'http://localhost:8080/snippet.js', + timeout: 5000, + }); + + pending.resolve(createAgent().agent); + await flush(); + expect(view.value.status).toBe('ready'); + expect(view.value.error).toBeNull(); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('passes only the options that are set', async () => { + loadMock.mockResolvedValue(createAgent().agent); + renderInProvider(() => useShieldLabs()); + await flush(); + expect(loadMock.mock.calls[0]?.[0]).toStrictEqual({ publicKey: PUBLIC_KEY }); + }); + + it('goes to error with the ShieldLabsError of a failed load', async () => { + const failure = new ShieldLabsError('load_failed', 'Could not load the agent.'); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs()); + expect(view.value.status).toBe('loading'); + await flush(); + expect(view.value.status).toBe('error'); + expect(view.value.error).toBe(failure); + }); + + it('wraps an unexpected failure (also a synchronous throw) in a load_failed ShieldLabsError', async () => { + const cause = new TypeError('unexpected'); + loadMock.mockImplementation(() => { + throw cause; + }); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + expect(view.value.status).toBe('error'); + expect(view.value.error).toBeInstanceOf(ShieldLabsError); + expect(view.value.error).toMatchObject({ code: 'load_failed', cause }); + }); + + it.each([ + ['invalid options', new ShieldLabsError('invalid_options', 'publicKey must match ^[A-Za-z0-9_-]{1,128}$.')], + ['a page that is not a secure context', new ShieldLabsError('unsupported_environment', 'The page is not a secure context.')], + ])('warns once in the console for %s, also in StrictMode', async (_, failure) => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs(), {}, { strict: true }); + await flush(); + view.rerender({}); + await flush(); + expect(view.value).toMatchObject({ status: 'error', error: failure }); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith('[ShieldLabs] ShieldLabsProvider could not load the agent: ' + failure.message); + }); + + it.each([ + ['a failed download', new ShieldLabsError('load_failed', 'Network error.')], + ['a load that timed out', new ShieldLabsError('timeout', 'The ShieldLabs agent did not load within 10000 ms.')], + ])('does not warn for %s, which happens to some visitors', async (_, failure) => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + expect(view.value.status).toBe('error'); + expect(warn).not.toHaveBeenCalled(); + }); + + it('does not warn when a load for older options fails after the options changed', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const first = deferred(); + loadMock.mockReturnValueOnce(first.promise).mockResolvedValueOnce(createAgent().agent); + const view = renderInProvider(() => useShieldLabs(), { publicKey: 'not a key' }); + view.rerender({ publicKey: PUBLIC_KEY }); + first.reject(new ShieldLabsError('invalid_options', 'publicKey must match ^[A-Za-z0-9_-]{1,128}$.')); + await flush(); + expect(view.value.status).toBe('ready'); + expect(warn).not.toHaveBeenCalled(); + }); + + it('does not load again when it renders again with the same options', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + view.rerender({}); + view.rerender({ checkOnLoad: false }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(view.value.status).toBe('ready'); + }); + + it('loads again when an option changes, shows loading and ignores the older load', async () => { + const first = deferred(); + const second = deferred(); + loadMock.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise); + const one = createAgent(); + const two = createAgent(); + const view = renderInProvider(() => useShieldLabs()); + + view.rerender({ publicKey: OTHER_PUBLIC_KEY }); + expect(loadMock).toHaveBeenCalledTimes(2); + expect(loadMock).toHaveBeenLastCalledWith({ publicKey: OTHER_PUBLIC_KEY }); + + first.resolve(one.agent); + await flush(); + expect(view.value.status).toBe('loading'); + + second.resolve(two.agent); + await flush(); + expect(view.value.status).toBe('ready'); + + await act(() => view.value.identify()); + expect(two.identify).toHaveBeenCalledTimes(1); + expect(one.identify).not.toHaveBeenCalled(); + }); + + it.each(['loads', 'fails'] as const)( + 'stays ready when the load for older options %s after the load for the new options', + async (outcome) => { + const first = deferred(); + const second = deferred(); + loadMock.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise); + const one = createAgent(); + const two = createAgent(); + const view = renderInProvider(() => useShieldLabs()); + view.rerender({ publicKey: OTHER_PUBLIC_KEY }); + + second.resolve(two.agent); + await flush(); + expect(view.value.status).toBe('ready'); + + if (outcome === 'loads') first.resolve(one.agent); + else first.reject(new ShieldLabsError('load_failed', 'Network error.')); + await flush(); + expect(view.value).toMatchObject({ status: 'ready', error: null }); + + await act(() => view.value.identify()); + expect(two.identify).toHaveBeenCalledTimes(1); + expect(one.identify).not.toHaveBeenCalled(); + }, + ); + + it('shows loading in the same render when an option changes after it was ready', async () => { + loadMock.mockImplementation(() => Promise.resolve(createAgent().agent)); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + expect(view.value.status).toBe('ready'); + + view.rerender({ timeout: 3000 }); + expect(view.value.status).toBe('loading'); + await flush(); + expect(view.value.status).toBe('ready'); + expect(loadMock).toHaveBeenLastCalledWith({ publicKey: PUBLIC_KEY, timeout: 3000 }); + }); + + it('keeps identify, check, load and getAgent stable across renders', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs()); + const { identify, check, load: loadAgent, getAgent } = view.value; + await flush(); + view.rerender({ checkOnLoad: false }); + expect(view.value.identify).toBe(identify); + expect(view.value.check).toBe(check); + expect(view.value.load).toBe(loadAgent); + expect(view.value.getAgent).toBe(getAgent); + }); +}); + +describe('useShieldLabs().identify() and check()', () => { + it('pass the options through to the agent once it is ready', async () => { + const { agent, identify, check } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + + const result = await view.value.identify({ userId: USER_HID, timeout: 2000 }); + expect(result).toMatchObject({ userId: USER_HID }); + expect(identify).toHaveBeenCalledWith({ userId: USER_HID, timeout: 2000 }); + + check.mockResolvedValueOnce(null); + await expect(view.value.check()).resolves.toBeNull(); + expect(check).toHaveBeenCalledWith(undefined); + }); + + it('wait for the load instead of failing while the agent is loading', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify, check } = createAgent(); + const view = renderInProvider(() => useShieldLabs()); + + const identified = view.value.identify({ userId: USER_HID }); + const checked = view.value.check(); + const settled: string[] = []; + void identified.then(() => settled.push('identify')); + void checked.then(() => settled.push('check')); + await flush(); + expect(settled).toEqual([]); + expect(identify).not.toHaveBeenCalled(); + + pending.resolve(agent); + await flush(); + await expect(identified).resolves.toMatchObject({ userId: USER_HID }); + await expect(checked).resolves.toMatchObject({ userId: null }); + expect(identify).toHaveBeenCalledTimes(1); + expect(check).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it.each(['invalid_options', 'unsupported_environment'] as const)( + 'reject with the load error and do not load again after %s', + async (code) => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const failure = new ShieldLabsError(code, 'Cannot load here.'); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + + await expect(view.value.identify()).rejects.toBe(failure); + await expect(view.value.check()).rejects.toBe(failure); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(view.value.status).toBe('error'); + }, + ); + + it.each([ + ['a failed import', new ShieldLabsError('load_failed', 'Network error.')], + ['a load that timed out', new ShieldLabsError('timeout', 'The ShieldLabs agent did not load within 10000 ms.')], + ])('load the agent again after %s, and the provider becomes ready', async (_, failure) => { + const { agent, identify } = createAgent(); + loadMock.mockRejectedValueOnce(failure).mockResolvedValueOnce(agent); + const view = renderInProvider(() => useShieldLabs()); + await flush(); + expect(view.value.status).toBe('error'); + expect(view.value.error).toBe(failure); + + let identified: Promise | undefined; + act(() => { + identified = view.value.identify(); + }); + expect(view.value.status).toBe('loading'); + expect(view.value.error).toBeNull(); + + await flush(); + expect(view.value.status).toBe('ready'); + await expect(identified).resolves.toMatchObject({ userId: null }); + expect(identify).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledTimes(2); + }); + + it('give up waiting for the load after the call timeout, then the provider timeout', async () => { + vi.useFakeTimers(); + loadMock.mockReturnValue(new Promise(() => undefined)); + const view = renderInProvider(() => useShieldLabs(), { timeout: 3000 }); + + const outcomes: string[] = []; + view.value.identify({ timeout: 50 }).catch((error: unknown) => { + outcomes.push('identify ' + (error as ShieldLabsError).code); + }); + view.value.check().catch((error: unknown) => { + outcomes.push('check ' + (error as ShieldLabsError).code); + }); + + await vi.advanceTimersByTimeAsync(49); + expect(outcomes).toEqual([]); + await vi.advanceTimersByTimeAsync(1); + expect(outcomes).toEqual(['identify timeout']); + await vi.advanceTimersByTimeAsync(2950); + expect(outcomes).toEqual(['identify timeout', 'check timeout']); + expect(vi.getTimerCount()).toBe(0); + expect(view.value.status).toBe('loading'); + }); + + it('wait 10 seconds by default and leave no timer behind when the load wins', async () => { + vi.useFakeTimers(); + const pending = deferred(); + loadMock.mockReturnValueOnce(new Promise(() => undefined)); + const view = renderInProvider(() => useShieldLabs()); + + let failure: unknown; + view.value.identify().catch((error: unknown) => { + failure = error; + }); + await vi.advanceTimersByTimeAsync(9999); + expect(failure).toBeUndefined(); + await vi.advanceTimersByTimeAsync(1); + expect(failure).toBeInstanceOf(ShieldLabsError); + expect(failure).toMatchObject({ code: 'timeout', message: 'The ShieldLabs agent did not load within 10000 ms.' }); + + // With other options the agent loads in time: the wait timer is cleared. + loadMock.mockReturnValueOnce(pending.promise); + view.rerender({ publicKey: OTHER_PUBLIC_KEY }); + const identified = view.value.identify(); + expect(vi.getTimerCount()).toBe(1); + await act(async () => { + pending.resolve(createAgent().agent); + await vi.advanceTimersByTimeAsync(0); + }); + await expect(identified).resolves.toMatchObject({ userId: null }); + expect(vi.getTimerCount()).toBe(0); + expect(view.value.status).toBe('ready'); + }); + + it('keep the whole call within its timeout when it waits for the load', async () => { + vi.useFakeTimers(); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify, check } = createAgent(); + // An agent that never answers: each call rejects when the timeout it was given runs out. + const silent = (options?: IdentifyOptions): Promise => + new Promise((_, reject) => { + setTimeout(() => { + reject(new ShieldLabsError('timeout', 'The agent did not answer.')); + }, options?.timeout ?? 60000); + }); + identify.mockImplementation(silent); + check.mockImplementation(silent); + const view = renderInProvider(() => useShieldLabs(), { timeout: 8000 }); + + const start = Date.now(); + let identifyFailedAt: number | undefined; + let checkFailedAt: number | undefined; + view.value.identify({ timeout: 3000, userId: USER_HID }).catch(() => { + identifyFailedAt = Date.now(); + }); + view.value.check().catch(() => { + checkFailedAt = Date.now(); + }); + + // The agent loads after 2.9 seconds: the calls get what is left of their timeouts. + await vi.advanceTimersByTimeAsync(2900); + await act(async () => { + pending.resolve(agent); + await vi.advanceTimersByTimeAsync(0); + }); + expect(identify).toHaveBeenCalledWith({ timeout: 100, userId: USER_HID }); + expect(check).toHaveBeenCalledWith({ timeout: 5100 }); + + await vi.advanceTimersByTimeAsync(100); + expect(identifyFailedAt).toBe(start + 3000); + expect(checkFailedAt).toBeUndefined(); + await vi.advanceTimersByTimeAsync(5000); + expect(checkFailedAt).toBe(start + 8000); + expect(vi.getTimerCount()).toBe(0); + }); + + it('give the agent what is left of the default timeout, and pass options it must reject unchanged', async () => { + vi.useFakeTimers(); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify, check } = createAgent(); + const view = renderInProvider(() => useShieldLabs()); + + const identified = view.value.identify(); + const badTimeout = view.value.identify({ timeout: -1 }); + const checked = view.value.check('not options' as unknown as IdentifyOptions); + await vi.advanceTimersByTimeAsync(4000); + await act(async () => { + pending.resolve(agent); + await vi.advanceTimersByTimeAsync(0); + }); + + await expect(identified).resolves.toMatchObject({ userId: null }); + await expect(badTimeout).resolves.toMatchObject({ userId: null }); + await expect(checked).resolves.toMatchObject({ userId: null }); + expect(identify).toHaveBeenNthCalledWith(1, { timeout: 6000 }); + expect(identify).toHaveBeenNthCalledWith(2, { timeout: -1 }); + expect(check).toHaveBeenCalledWith('not options'); + }); + + it('never give the agent more than the call timeout, also when the clock is set back', async () => { + vi.useFakeTimers(); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify } = createAgent(); + const view = renderInProvider(() => useShieldLabs(), { timeout: 5000 }); + + const identified = view.value.identify(); + await vi.advanceTimersByTimeAsync(1000); + vi.setSystemTime(Date.now() - 60000); + await act(async () => { + pending.resolve(agent); + await vi.advanceTimersByTimeAsync(0); + }); + await expect(identified).resolves.toMatchObject({ userId: null }); + expect(identify).toHaveBeenCalledWith({ timeout: 5000 }); + }); + + it('settle calls made before the provider unmounts', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const view = renderInProvider(() => useShieldLabs()); + const identified = view.value.identify(); + view.unmount(); + + pending.resolve(createAgent().agent); + await flush(); + await expect(identified).resolves.toMatchObject({ userId: null }); + }); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..37378fd --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,30 @@ +import { afterEach, beforeEach, vi } from 'vitest'; + +declare global { + var IS_REACT_ACT_ENVIRONMENT: boolean | undefined; +} + +// Makes React report state updates outside act() (the testing library only sets this when test +// globals are enabled, which this project does not use). +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +// Every test fails on an unexpected console.error, which is how React reports updates outside +// act(), errors during render and similar problems. A test that expects an error calls +// `expectConsoleError()` from `./support/console`. +beforeEach(() => { + vi.spyOn(console, 'error'); +}); + +afterEach(async () => { + vi.useRealTimers(); + // Unmount what the test rendered. Imported lazily so that tests in the node environment (server-side + // rendering, the built files) do not load the DOM testing library. + if (typeof document !== 'undefined') { + const { cleanup } = await import('@testing-library/react'); + cleanup(); + } + const calls = vi.mocked(console.error).mock.calls; + if (calls.length > 0) { + throw new Error('Unexpected console.error: ' + calls.map((args) => args.map(String).join(' ')).join('\n')); + } +}); diff --git a/test/ssr.test.tsx b/test/ssr.test.tsx new file mode 100644 index 0000000..20dba55 --- /dev/null +++ b/test/ssr.test.tsx @@ -0,0 +1,84 @@ +// @vitest-environment node +import { StrictMode } from 'react'; +import { renderToString } from 'react-dom/server'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { load } from '@shieldlabs-ai/js'; +import { ShieldLabsProvider, useIdentify, useShieldLabs } from '../src'; +import { PUBLIC_KEY, USER_HID } from './support/agent'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +function Status() { + const { status, error } = useShieldLabs(); + const { isLoading, result } = useIdentify({ runOnMount: true, userId: USER_HID }); + return

{`status=${status} error=${String(error)} isLoading=${String(isLoading)} result=${JSON.stringify(result)}`}

; +} + +// Records every read of `window` or `document`, including `typeof window` checks. +const touched: string[] = []; + +beforeEach(() => { + touched.length = 0; + for (const name of ['window', 'document']) { + Object.defineProperty(globalThis, name, { + configurable: true, + get() { + touched.push(name); + return undefined; + }, + }); + } +}); + +afterEach(() => { + for (const name of ['window', 'document']) { + Reflect.deleteProperty(globalThis, name); + } +}); + +describe('server-side rendering', () => { + it('renders the loading state, touches no browser global and loads nothing', () => { + const html = renderToString( + + + , + ); + expect(html).toBe('

status=loading error=null isLoading=true result=null

'); + expect(touched).toEqual([]); + expect(load).not.toHaveBeenCalled(); + }); + + it('renders with autoLoad={false} and loads nothing', () => { + const html = renderToString( + + + , + ); + expect(html).toBe('

status=loading error=null isLoading=true result=null

'); + expect(touched).toEqual([]); + expect(load).not.toHaveBeenCalled(); + }); + + it('also renders in StrictMode with every provider option set', () => { + const html = renderToString( + + + + + , + ); + expect(html).toContain('status=loading'); + expect(touched).toEqual([]); + expect(load).not.toHaveBeenCalled(); + }); +}); diff --git a/test/strict-mode.test.tsx b/test/strict-mode.test.tsx new file mode 100644 index 0000000..2501c61 --- /dev/null +++ b/test/strict-mode.test.tsx @@ -0,0 +1,76 @@ +import { act } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, type IdentifyResult } from '@shieldlabs-ai/js'; +import { useIdentify, useShieldLabs } from '../src'; +import { createAgent, USER_HID } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +describe('React StrictMode (effects run twice on mount)', () => { + it('loads the agent once and becomes ready', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs(), {}, { strict: true }); + expect(view.value.status).toBe('loading'); + await flush(); + expect(view.value.status).toBe('ready'); + expect(loadMock).toHaveBeenCalledTimes(1); + }); + + it('runs checkOnLoad once', async () => { + const { agent, check } = createAgent(); + loadMock.mockResolvedValue(agent); + renderInProvider(() => useShieldLabs(), { checkOnLoad: { userId: USER_HID } }, { strict: true }); + await flush(); + expect(check).toHaveBeenCalledTimes(1); + }); + + it('runs runOnMount once and stores its result', async () => { + const { agent, identify } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useIdentify({ runOnMount: true }), {}, { strict: true }); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(view.value).toMatchObject({ result: { userId: null }, isLoading: false, error: null }); + }); + + it('with autoLoad={false}, loads nothing until load(), then loads once', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }, { strict: true }); + await flush(); + expect(loadMock).not.toHaveBeenCalled(); + act(() => { + view.value.load(); + view.value.load(); + }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(1); + expect(view.value.status).toBe('ready'); + }); + + it('skips checkOnLoad while runOnMount identifies the same User HID', async () => { + const { agent, check, identify } = createAgent(); + identify.mockReturnValueOnce(new Promise(() => undefined)); + loadMock.mockResolvedValue(agent); + renderInProvider(() => useIdentify({ runOnMount: true, userId: USER_HID }), { checkOnLoad: { userId: USER_HID } }, { strict: true }); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + expect(check).not.toHaveBeenCalled(); + }); + + it('calls load() again for a new provider mount, which @shieldlabs-ai/js memoizes', async () => { + loadMock.mockResolvedValue(createAgent().agent); + const first = renderInProvider(() => useShieldLabs(), {}, { strict: true }); + await flush(); + first.unmount(); + renderInProvider(() => useShieldLabs(), {}, { strict: true }); + await flush(); + expect(loadMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/support/agent.ts b/test/support/agent.ts new file mode 100644 index 0000000..75e812b --- /dev/null +++ b/test/support/agent.ts @@ -0,0 +1,58 @@ +import { vi } from 'vitest'; +import type { IdentifyOptions, IdentifyResult, InteractionIdentifier, ShieldLabsAgent } from '@shieldlabs-ai/js'; + +/** Placeholder Public Keys in the issued format (32 lowercase hex characters). */ +export const PUBLIC_KEY = '0123456789abcdef0123456789abcdef'; +export const OTHER_PUBLIC_KEY = 'fedcba9876543210fedcba9876543210'; + +/** A User HID as a server would compute it (64 hex characters). */ +export const USER_HID = '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'; +export const OTHER_USER_HID = '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752'; + +let counter = 0; + +/** UUIDv4-shaped request IDs, unique within a test run. */ +export function nextRequestId(): string { + counter += 1; + return '6f1c2a9e-7b3d-4c5e-8f90-' + counter.toString(16).padStart(12, '0'); +} + +export interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(reason: unknown): void; +} + +export function deferred(): Deferred { + let resolve!: (value: T) => void; + let reject!: (reason: unknown) => void; + const promise = new Promise((onResolve, onReject) => { + resolve = onResolve; + reject = onReject; + }); + return { promise, resolve, reject }; +} + +function answer(options?: IdentifyOptions): Promise { + return Promise.resolve({ requestId: nextRequestId(), userId: options?.userId ?? null }); +} + +/** + * A stand-in for the agent that `load()` of `@shieldlabs-ai/js` resolves. `identify()` and `check()` + * answer with a new request ID and echo the User HID. `identifyOnInteraction()` returns `handle`, + * whose `take()` answers the same way. Create it inside each test: the config resets mocks before + * every test. + */ +export function createAgent() { + const identify = vi.fn<(options?: IdentifyOptions) => Promise>(answer); + const check = vi.fn<(options?: IdentifyOptions) => Promise>(answer); + const handle = { + take: vi.fn<() => Promise>(() => answer()), + dispose: vi.fn<() => void>(), + } satisfies InteractionIdentifier; + const identifyOnInteraction = vi.fn<(target: EventTarget, options?: IdentifyOptions) => InteractionIdentifier>( + () => handle, + ); + const agent: ShieldLabsAgent = { identify, check, identifyOnInteraction }; + return { agent, identify, check, identifyOnInteraction, handle }; +} diff --git a/test/support/console.ts b/test/support/console.ts new file mode 100644 index 0000000..8d35f92 --- /dev/null +++ b/test/support/console.ts @@ -0,0 +1,13 @@ +import { vi } from 'vitest'; + +/** Silences console.error for this test and returns the calls it received so far and later. */ +export function expectConsoleError(): unknown[][] { + const spy = vi.mocked(console.error); + spy.mockImplementation(() => undefined); + return spy.mock.calls; +} + +/** Forgets the console.error calls of this test (after asserting on them). */ +export function clearConsoleError(): void { + vi.mocked(console.error).mockClear(); +} diff --git a/test/support/render.tsx b/test/support/render.tsx new file mode 100644 index 0000000..0788175 --- /dev/null +++ b/test/support/render.tsx @@ -0,0 +1,67 @@ +import { StrictMode, type ReactElement } from 'react'; +import { act, render } from '@testing-library/react'; +import { ShieldLabsProvider, type ShieldLabsProviderProps } from '../../src'; +import { PUBLIC_KEY } from './agent'; + +type ProviderProps = Partial>; + +export interface RenderedHook { + /** The value the hook returned in the latest render. */ + readonly value: T; + /** How many times the component that calls the hook rendered. */ + readonly renders: number; + rerender(props?: ProviderProps): void; + unmount(): void; +} + +/** + * Renders a component that calls `useValue()` inside a `ShieldLabsProvider` (with a placeholder + * Public Key unless `props` sets one), optionally in `StrictMode`. + */ +export function renderInProvider( + useValue: () => T, + props: ProviderProps = {}, + options: { strict?: boolean } = {}, +): RenderedHook { + const seen: { value?: T; renders: number } = { renders: 0 }; + + function Consumer(): null { + seen.value = useValue(); + seen.renders += 1; + return null; + } + + const tree = (next: ProviderProps): ReactElement => { + const provider = ( + + + + ); + return options.strict ? {provider} : provider; + }; + + const utils = render(tree(props)); + + return { + get value(): T { + if (seen.value === undefined) throw new Error('The component has not rendered.'); + return seen.value; + }, + get renders(): number { + return seen.renders; + }, + rerender(next: ProviderProps = props) { + utils.rerender(tree(next)); + }, + unmount() { + utils.unmount(); + }, + }; +} + +/** Lets pending promises settle and React apply the updates they cause. Needs real timers. */ +export async function flush(): Promise { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} diff --git a/test/types.test.ts b/test/types.test.ts new file mode 100644 index 0000000..508636e --- /dev/null +++ b/test/types.test.ts @@ -0,0 +1,79 @@ +// Compile-time checks of the public types. `npm run typecheck` verifies them; at runtime they are no-ops. +import { describe, expectTypeOf, it } from 'vitest'; +import { + ShieldLabsError, + ShieldLabsProvider, + useIdentify, + useShieldLabs, + type IdentifyOptions, + type IdentifyResult, + type InteractionIdentifier, + type LoadOptions, + type ShieldLabsAgent, + type ShieldLabsProviderProps, + type ShieldLabsStatus, + type UseIdentifyOptions, + type UseIdentifyResult, + type UseShieldLabsResult, +} from '../src'; + +describe('public types', () => { + it('give the browser only the request ID and the User HID', () => { + expectTypeOf().toEqualTypeOf<'requestId' | 'userId'>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it('describe the provider props', () => { + expectTypeOf().toExtend(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<'production' | 'development' | undefined>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + // @ts-expect-error publicKey is required + const missingKey: ShieldLabsProviderProps = {}; + expectTypeOf(missingKey).not.toBeAny(); + expectTypeOf(ShieldLabsProvider).parameter(0).toEqualTypeOf(); + }); + + it('describe useShieldLabs()', () => { + expectTypeOf(useShieldLabs).parameters.toEqualTypeOf<[]>(); + expectTypeOf(useShieldLabs).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<'loading' | 'ready' | 'error'>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<(options?: IdentifyOptions) => Promise>(); + expectTypeOf().toEqualTypeOf< + (options?: IdentifyOptions) => Promise + >(); + expectTypeOf().toEqualTypeOf<() => void>(); + expectTypeOf().toEqualTypeOf<() => Promise>(); + expectTypeOf().toEqualTypeOf<'status' | 'error' | 'identify' | 'check' | 'load' | 'getAgent'>(); + }); + + it('re-export the agent types of @shieldlabs-ai/js for getAgent()', () => { + expectTypeOf().toEqualTypeOf< + (target: EventTarget, options?: IdentifyOptions) => InteractionIdentifier + >(); + expectTypeOf().toEqualTypeOf<() => Promise>(); + expectTypeOf().toEqualTypeOf<() => void>(); + }); + + it('describe useIdentify()', () => { + expectTypeOf(useIdentify).parameter(0).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<'userId' | 'runOnMount'>(); + expectTypeOf(useIdentify).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<'identify' | 'result' | 'isLoading' | 'error' | 'reset'>(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + (options?: IdentifyOptions) => Promise + >(); + }); + + it('re-export the error class of @shieldlabs-ai/js', () => { + expectTypeOf(new ShieldLabsError('timeout', 'message').code).toEqualTypeOf< + 'invalid_options' | 'unsupported_environment' | 'load_failed' | 'not_initialized' | 'timeout' + >(); + }); +}); diff --git a/test/unmount.test.tsx b/test/unmount.test.tsx new file mode 100644 index 0000000..7d340d0 --- /dev/null +++ b/test/unmount.test.tsx @@ -0,0 +1,111 @@ +import { describe, expect, it, vi } from 'vitest'; +import { load, type IdentifyResult, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { useIdentify, useShieldLabs } from '../src'; +import { createAgent, deferred } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +/** Counts the state setter calls of the package, and those made after the test unmounted. */ +const updates = vi.hoisted(() => ({ total: 0, afterUnmount: 0, unmounted: false })); + +// The package's own imports of React get a useState whose setter is counted. React itself and the +// testing library keep the real module. +vi.mock('react', async (importOriginal) => { + const actual = await importOriginal(); + const useState = ((initial: unknown) => { + const [state, setState] = actual.useState(initial); + const countedSetState = (next: unknown): void => { + updates.total += 1; + if (updates.unmounted) updates.afterUnmount += 1; + setState(next); + }; + return [state, countedSetState]; + }) as typeof actual.useState; + return { ...actual, useState, default: { ...actual, useState } }; +}); + +const loadMock = vi.mocked(load); + +function unmountNow(view: { unmount(): void }): void { + view.unmount(); + updates.unmounted = true; +} + +describe('after unmount', () => { + it('the provider makes no state update when the load settles', async () => { + Object.assign(updates, { total: 0, afterUnmount: 0, unmounted: false }); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const view = renderInProvider(() => useShieldLabs()); + unmountNow(view); + + pending.resolve(createAgent().agent); + await flush(); + expect(updates.afterUnmount).toBe(0); + }); + + it('the provider makes no state update when a failed load settles', async () => { + Object.assign(updates, { total: 0, afterUnmount: 0, unmounted: false }); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const view = renderInProvider(() => useShieldLabs()); + unmountNow(view); + + pending.reject(new Error('offline')); + await flush(); + expect(updates.afterUnmount).toBe(0); + }); + + it('useIdentify() makes no state update when a running identification settles', async () => { + Object.assign(updates, { total: 0, afterUnmount: 0, unmounted: false }); + const { agent, identify } = createAgent(); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useIdentify({ runOnMount: true })); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + const before = updates.total; + expect(before).toBeGreaterThan(0); + + unmountNow(view); + running.resolve({ requestId: '4f5a6b7c-8d9e-4f0a-9b1c-2d3e4f5a6b7c', userId: null }); + await flush(); + expect(updates.afterUnmount).toBe(0); + }); + + it('a stale load() or getAgent() of a provider with autoLoad={false} makes no state update', async () => { + Object.assign(updates, { total: 0, afterUnmount: 0, unmounted: false }); + const { agent } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useShieldLabs(), { autoLoad: false }); + await flush(); + const { load: loadAgent, getAgent } = view.value; + + unmountNow(view); + const waiting = getAgent(); + loadAgent(); + await expect(waiting).resolves.toBe(agent); + await flush(); + expect(updates.afterUnmount).toBe(0); + }); + + it('a stale identify() or reset() makes no state update and still settles', async () => { + Object.assign(updates, { total: 0, afterUnmount: 0, unmounted: false }); + const { agent } = createAgent(); + loadMock.mockResolvedValue(agent); + const view = renderInProvider(() => useIdentify()); + await flush(); + const { identify, reset } = view.value; + + unmountNow(view); + reset(); + await expect(identify()).resolves.toMatchObject({ userId: null }); + await flush(); + expect(updates.afterUnmount).toBe(0); + }); +}); diff --git a/test/use-identify.test.tsx b/test/use-identify.test.tsx new file mode 100644 index 0000000..90e37f3 --- /dev/null +++ b/test/use-identify.test.tsx @@ -0,0 +1,653 @@ +import { act } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { load, ShieldLabsError, type IdentifyOptions, type IdentifyResult, type ShieldLabsAgent } from '@shieldlabs-ai/js'; +import { useIdentify, type UseIdentifyOptions } from '../src'; +import { createAgent, deferred, OTHER_USER_HID, USER_HID } from './support/agent'; +import { flush, renderInProvider } from './support/render'; + +vi.mock('@shieldlabs-ai/js', async (importOriginal) => ({ + ...(await importOriginal()), + load: vi.fn(), +})); + +const loadMock = vi.mocked(load); + +/** Renders `useIdentify(options)` in a provider whose agent has loaded. */ +async function renderReady(options?: UseIdentifyOptions) { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + const view = renderInProvider(() => useIdentify(options)); + await flush(); + return { ...fake, view }; +} + +describe('useIdentify()', () => { + it('starts idle and does not identify on mount by default', async () => { + const { view, identify } = await renderReady(); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: null }); + expect(identify).not.toHaveBeenCalled(); + }); + + it('identify() shows loading, then stores and resolves the result', async () => { + const { view, identify } = await renderReady(); + let identified: Promise | undefined; + act(() => { + identified = view.value.identify(); + }); + expect(view.value.isLoading).toBe(true); + + await flush(); + const result = await identified; + expect(result).toEqual({ requestId: expect.any(String) as string, userId: null }); + expect(view.value).toMatchObject({ result, isLoading: false, error: null }); + expect(identify).toHaveBeenCalledTimes(1); + expect(identify).toHaveBeenCalledWith({}); + }); + + it('passes the userId of the hook through', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + await act(() => view.value.identify()); + expect(identify).toHaveBeenCalledWith({ userId: USER_HID }); + expect(view.value.result).toMatchObject({ userId: USER_HID }); + }); + + it('lets a userId passed to identify() override the hook option, and keeps other options', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + await act(() => view.value.identify({ userId: OTHER_USER_HID })); + expect(identify).toHaveBeenLastCalledWith({ userId: OTHER_USER_HID }); + + await act(() => view.value.identify({ timeout: 2500 })); + expect(identify).toHaveBeenLastCalledWith({ timeout: 2500, userId: USER_HID }); + + const anonymous = await renderReady(); + await act(() => anonymous.view.value.identify({ userId: OTHER_USER_HID })); + expect(anonymous.identify).toHaveBeenLastCalledWith({ userId: OTHER_USER_HID }); + }); + + it('lets a userId key set to undefined or null override the hook option: both identify anonymously', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + await act(() => view.value.identify({ userId: undefined })); + expect(identify).toHaveBeenLastCalledWith({}); + expect(view.value.result).toMatchObject({ userId: null }); + + // null from plain JavaScript: anonymous as well, and no null reaches the agent. + await act(() => view.value.identify({ userId: null as unknown as string, timeout: 2500 })); + expect(identify).toHaveBeenLastCalledWith({ timeout: 2500 }); + expect(view.value.result).toMatchObject({ userId: null }); + + // Only options without the key use the User HID of the hook. + await act(() => view.value.identify({})); + expect(identify).toHaveBeenLastCalledWith({ userId: USER_HID }); + expect(view.value.result).toMatchObject({ userId: USER_HID }); + }); + + it('passes options that are not an object to the agent unchanged, so that it can reject them', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + const notOptions = 'not options' as unknown as IdentifyOptions; + await act(() => view.value.identify(notOptions)); + expect(identify).toHaveBeenLastCalledWith('not options'); + + await act(() => view.value.identify(null as unknown as IdentifyOptions)); + expect(identify).toHaveBeenLastCalledWith({ userId: USER_HID }); + }); + + it('stores a failure in error and resolves null instead of rejecting', async () => { + const { view, identify } = await renderReady(); + const failure = new ShieldLabsError('not_initialized', 'The agent did not start an identification.'); + identify.mockRejectedValueOnce(failure); + + let outcome: IdentifyResult | null | undefined; + await act(async () => { + outcome = await view.value.identify(); + }); + expect(outcome).toBeNull(); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: failure }); + }); + + it('wraps an unexpected failure in a ShieldLabsError', async () => { + const { view, identify } = await renderReady(); + const cause = new Error('unexpected'); + identify.mockRejectedValueOnce(cause); + + let outcome: IdentifyResult | null | undefined; + await act(async () => { + outcome = await view.value.identify(); + }); + expect(outcome).toBeNull(); + expect(view.value.error).toBeInstanceOf(ShieldLabsError); + expect(view.value.error).toMatchObject({ code: 'not_initialized', cause }); + }); + + it('leaves no unhandled rejection when a failed identify() is not awaited', async () => { + const { view, identify } = await renderReady(); + identify.mockRejectedValueOnce(new ShieldLabsError('timeout', 'The agent did not answer.')); + act(() => { + void view.value.identify(); + }); + await flush(); + expect(view.value.error).toMatchObject({ code: 'timeout' }); + }); + + it('clears the previous result and error while a new identification runs', async () => { + const { view, identify } = await renderReady(); + await act(() => view.value.identify()); + expect(view.value.result).not.toBeNull(); + + const pending = deferred(); + identify.mockReturnValueOnce(pending.promise); + act(() => { + void view.value.identify(); + }); + expect(view.value).toMatchObject({ result: null, isLoading: true, error: null }); + + pending.resolve({ requestId: '0b9f8e7d-6c5b-4a39-8281-7f6e5d4c3b2a', userId: null }); + await flush(); + expect(view.value.result).toEqual({ requestId: '0b9f8e7d-6c5b-4a39-8281-7f6e5d4c3b2a', userId: null }); + + identify.mockRejectedValueOnce(new ShieldLabsError('timeout', 'The agent did not answer.')); + await act(() => view.value.identify()); + expect(view.value.error).not.toBeNull(); + const next = deferred(); + identify.mockReturnValueOnce(next.promise); + act(() => { + void view.value.identify(); + }); + expect(view.value).toMatchObject({ result: null, isLoading: true, error: null }); + next.resolve({ requestId: '5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d', userId: null }); + await flush(); + }); + + it('keeps the state of the latest call when an earlier one settles later', async () => { + const { view, identify } = await renderReady(); + const slow = deferred(); + const fast = deferred(); + identify.mockReturnValueOnce(slow.promise).mockReturnValueOnce(fast.promise); + + let first: Promise | undefined; + act(() => { + first = view.value.identify(); + // Another timeout: a separate identification, not the running one. + void view.value.identify({ timeout: 5000 }); + }); + await flush(); + expect(identify).toHaveBeenCalledTimes(2); + fast.resolve({ requestId: '1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f', userId: null }); + await flush(); + expect(view.value.result?.requestId).toBe('1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f'); + + slow.resolve({ requestId: '2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a', userId: null }); + await flush(); + await expect(first).resolves.toMatchObject({ requestId: '2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a' }); + expect(view.value.result?.requestId).toBe('1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f'); + }); + + it('reset() clears result and error, and a running call no longer updates the state', async () => { + const { view, identify } = await renderReady(); + identify.mockRejectedValueOnce(new ShieldLabsError('timeout', 'The agent did not answer.')); + await act(() => view.value.identify()); + expect(view.value.error).not.toBeNull(); + + act(() => { + view.value.reset(); + }); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: null }); + + const pending = deferred(); + identify.mockReturnValueOnce(pending.promise); + let identified: Promise | undefined; + act(() => { + identified = view.value.identify(); + }); + act(() => { + view.value.reset(); + }); + pending.resolve({ requestId: '3e4f5a6b-7c8d-4e9f-8a1b-2c3d4e5f6a7b', userId: null }); + await flush(); + await expect(identified).resolves.toMatchObject({ requestId: '3e4f5a6b-7c8d-4e9f-8a1b-2c3d4e5f6a7b' }); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: null }); + }); + + it('keeps identify and reset stable, and makes a new identify when userId changes', async () => { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + let options: UseIdentifyOptions = { userId: USER_HID }; + const view = renderInProvider(() => useIdentify(options)); + await flush(); + const { identify, reset } = view.value; + + view.rerender(); + expect(view.value.identify).toBe(identify); + expect(view.value.reset).toBe(reset); + + options = { userId: OTHER_USER_HID }; + view.rerender(); + expect(view.value.identify).not.toBe(identify); + expect(view.value.reset).toBe(reset); + await act(() => view.value.identify()); + expect(fake.identify).toHaveBeenCalledWith({ userId: OTHER_USER_HID }); + }); + + it('waits for the provider to load when called before the agent is ready', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify } = createAgent(); + const view = renderInProvider(() => useIdentify()); + + let identified: Promise | undefined; + act(() => { + identified = view.value.identify(); + }); + expect(view.value.isLoading).toBe(true); + expect(identify).not.toHaveBeenCalled(); + + pending.resolve(agent); + await flush(); + await expect(identified).resolves.toMatchObject({ userId: null }); + expect(view.value.isLoading).toBe(false); + }); + + it('reports the load error when the agent cannot load', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const failure = new ShieldLabsError('unsupported_environment', 'Not a secure context.'); + loadMock.mockRejectedValue(failure); + const view = renderInProvider(() => useIdentify({ runOnMount: true })); + await flush(); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: failure }); + await act(async () => { + expect(await view.value.identify()).toBeNull(); + }); + }); +}); + +describe('useIdentify() while an identification runs', () => { + it('returns the running call for a second call with the same User HID and timeout (a double submit)', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + + let first: Promise | undefined; + let second: Promise | undefined; + let third: Promise | undefined; + act(() => { + first = view.value.identify(); + second = view.value.identify(); + // The User HID of the hook, passed explicitly: the same identification. + third = view.value.identify({ userId: USER_HID }); + }); + expect(second).toBe(first); + expect(third).toBe(first); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + expect(view.value.isLoading).toBe(true); + + const result = { requestId: '8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e', userId: USER_HID }; + running.resolve(result); + await flush(); + await expect(first).resolves.toBe(result); + await expect(second).resolves.toBe(result); + expect(view.value).toMatchObject({ result, isLoading: false, error: null }); + }); + + it('starts another identification for another User HID or another timeout', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + identify.mockReturnValue(new Promise(() => undefined)); + act(() => { + void view.value.identify(); + void view.value.identify({ userId: OTHER_USER_HID }); + void view.value.identify({ timeout: 2000 }); + void view.value.identify({ timeout: 2000 }); + }); + await flush(); + expect(identify).toHaveBeenCalledTimes(3); + expect(identify.mock.calls.map(([options]) => options)).toEqual([ + { userId: USER_HID }, + { userId: OTHER_USER_HID }, + { timeout: 2000, userId: USER_HID }, + ]); + }); + + it('shares on the effective User HID: a userId key set to undefined or null is an anonymous call', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + identify.mockReturnValue(new Promise(() => undefined)); + + let forUser: Promise | undefined; + let anonymous: Promise | undefined; + let alsoAnonymous: Promise | undefined; + let againForUser: Promise | undefined; + act(() => { + forUser = view.value.identify(); + anonymous = view.value.identify({ userId: undefined }); + alsoAnonymous = view.value.identify({ userId: null as unknown as string }); + againForUser = view.value.identify({}); + }); + expect(anonymous).not.toBe(forUser); + expect(alsoAnonymous).toBe(anonymous); + expect(againForUser).toBe(forUser); + await flush(); + expect(identify.mock.calls.map(([options]) => options)).toEqual([{ userId: USER_HID }, {}]); + + // In a hook without a User HID, identify() is an anonymous call too. + const withoutUser = await renderReady(); + withoutUser.identify.mockReturnValue(new Promise(() => undefined)); + let first: Promise | undefined; + let withNull: Promise | undefined; + act(() => { + first = withoutUser.view.value.identify(); + withNull = withoutUser.view.value.identify({ userId: null as unknown as string }); + }); + expect(withNull).toBe(first); + await flush(); + expect(withoutUser.identify).toHaveBeenCalledTimes(1); + expect(withoutUser.identify).toHaveBeenCalledWith({}); + }); + + it('counts a call without a timeout as one with the default of 10000 ms', async () => { + const { view, identify } = await renderReady({ userId: USER_HID }); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + + let first: Promise | undefined; + let second: Promise | undefined; + let third: Promise | undefined; + act(() => { + first = view.value.identify(); + second = view.value.identify({ timeout: 10000 }); + third = view.value.identify({ userId: USER_HID, timeout: 10000 }); + }); + expect(second).toBe(first); + expect(third).toBe(first); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + // The agent gets the options of the first call; without a timeout it applies the same default. + expect(identify).toHaveBeenCalledWith({ userId: USER_HID }); + + const result = { requestId: 'cf3a4b5c-6d7e-4f80-9b9c-0d1e2f3a4b5c', userId: USER_HID }; + running.resolve(result); + await flush(); + await expect(first).resolves.toBe(result); + expect(view.value).toMatchObject({ result, isLoading: false, error: null }); + }); + + it('counts a call without a timeout as one with the provider timeout', async () => { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + const view = renderInProvider(() => useIdentify(), { timeout: 4000 }); + await flush(); + fake.identify.mockReturnValue(new Promise(() => undefined)); + + let first: Promise | undefined; + let second: Promise | undefined; + let other: Promise | undefined; + act(() => { + first = view.value.identify({ timeout: 4000 }); + second = view.value.identify(); + // 10000 ms is not the timeout this provider gives a call, so it is another identification. + other = view.value.identify({ timeout: 10000 }); + }); + expect(second).toBe(first); + expect(other).not.toBe(first); + await flush(); + expect(fake.identify.mock.calls.map(([options]) => options)).toEqual([{ timeout: 4000 }, { timeout: 10000 }]); + }); + + it('reads the provider timeout when a call starts', async () => { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + const view = renderInProvider(() => useIdentify(), { timeout: 4000 }); + await flush(); + fake.identify.mockReturnValue(new Promise(() => undefined)); + const { identify } = view.value; + let first: Promise | undefined; + act(() => { + first = identify(); + }); + + view.rerender({ timeout: 6000 }); + await flush(); + expect(view.value.identify).toBe(identify); + let later: Promise | undefined; + let sameTimeout: Promise | undefined; + act(() => { + // Runs with 6000 ms now: another identification. + later = identify(); + // The running call runs with 4000 ms. + sameTimeout = identify({ timeout: 4000 }); + }); + expect(later).not.toBe(first); + expect(sameTimeout).toBe(first); + await flush(); + expect(fake.identify).toHaveBeenCalledTimes(2); + }); + + it('never shares a call whose timeout @shieldlabs-ai/js rejects with one that can succeed', async () => { + const { view, identify } = await renderReady(); + identify.mockReturnValue(new Promise(() => undefined)); + let first: Promise | undefined; + const others: Promise[] = []; + act(() => { + first = view.value.identify(); + others.push(view.value.identify({ timeout: '10000' as unknown as number })); + others.push(view.value.identify({ timeout: null as unknown as number })); + }); + for (const other of others) expect(other).not.toBe(first); + await flush(); + expect(identify).toHaveBeenCalledTimes(3); + }); + + it('shares calls within one hook only', async () => { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + const view = renderInProvider(() => ({ one: useIdentify(), two: useIdentify() })); + await flush(); + fake.identify.mockReturnValue(new Promise(() => undefined)); + + let first: Promise | undefined; + let second: Promise | undefined; + act(() => { + first = view.value.one.identify(); + second = view.value.two.identify(); + }); + expect(second).not.toBe(first); + await flush(); + expect(fake.identify).toHaveBeenCalledTimes(2); + }); + + it('starts a new identification once the running one has settled, also after a failure', async () => { + const { view, identify } = await renderReady(); + await act(() => view.value.identify()); + identify.mockRejectedValueOnce(new ShieldLabsError('timeout', 'The agent did not answer.')); + await act(() => view.value.identify()); + await act(() => view.value.identify()); + expect(identify).toHaveBeenCalledTimes(3); + expect(view.value).toMatchObject({ result: { userId: null }, isLoading: false, error: null }); + }); + + it('shares a call that is still waiting for the agent to load', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify } = createAgent(); + const view = renderInProvider(() => useIdentify({ runOnMount: true })); + await flush(); + + let submitted: Promise | undefined; + act(() => { + submitted = view.value.identify(); + }); + pending.resolve(agent); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + await expect(submitted).resolves.toMatchObject({ userId: null }); + }); + + it('resolves null for every caller of a shared call that fails', async () => { + const { view, identify } = await renderReady(); + const running = deferred(); + identify.mockReturnValueOnce(running.promise); + let first: Promise | undefined; + let second: Promise | undefined; + act(() => { + first = view.value.identify(); + second = view.value.identify(); + }); + const failure = new ShieldLabsError('not_initialized', 'The agent did not start an identification.'); + running.reject(failure); + await flush(); + await expect(first).resolves.toBeNull(); + await expect(second).resolves.toBeNull(); + expect(identify).toHaveBeenCalledTimes(1); + expect(view.value).toMatchObject({ result: null, isLoading: false, error: failure }); + }); + + it('starts a new identification after reset()', async () => { + const { view, identify } = await renderReady(); + const stale = deferred(); + identify.mockReturnValueOnce(stale.promise); + act(() => { + void view.value.identify(); + }); + act(() => { + view.value.reset(); + }); + let fresh: Promise | undefined; + act(() => { + fresh = view.value.identify(); + }); + await flush(); + expect(identify).toHaveBeenCalledTimes(2); + const result = await fresh; + expect(view.value).toMatchObject({ result, isLoading: false, error: null }); + + stale.resolve({ requestId: '9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f', userId: null }); + await flush(); + expect(view.value.result).toBe(result); + }); + + it('shows the outcome of a shared earlier call when it was asked for last', async () => { + const { view, identify } = await renderReady(); + const mine = deferred(); + const other = deferred(); + identify.mockReturnValueOnce(mine.promise).mockReturnValueOnce(other.promise); + + let first: Promise | undefined; + let again: Promise | undefined; + act(() => { + first = view.value.identify({ userId: USER_HID }); + void view.value.identify({ userId: OTHER_USER_HID }); + }); + act(() => { + again = view.value.identify({ userId: USER_HID }); + }); + expect(again).toBe(first); + await flush(); + expect(identify).toHaveBeenCalledTimes(2); + + other.resolve({ requestId: 'ad1e2f3a-4b5c-4d6e-9f7a-8b9c0d1e2f3a', userId: OTHER_USER_HID }); + await flush(); + expect(view.value).toMatchObject({ result: null, isLoading: true }); + + mine.resolve({ requestId: 'be2f3a4b-5c6d-4e7f-8a8b-9c0d1e2f3a4b', userId: USER_HID }); + await flush(); + expect(view.value).toMatchObject({ result: { userId: USER_HID }, isLoading: false, error: null }); + }); + + it('never shares options that are not an object', async () => { + const { view, identify } = await renderReady(); + const notOptions = 'not options' as unknown as IdentifyOptions; + identify.mockReturnValue(new Promise(() => undefined)); + act(() => { + void view.value.identify(notOptions); + void view.value.identify(notOptions); + }); + await flush(); + expect(identify).toHaveBeenCalledTimes(2); + }); +}); + +describe('useIdentify() timeout', () => { + it('covers the wait for the agent: resolves null with a timeout error when the agent does not load in time', async () => { + vi.useFakeTimers(); + loadMock.mockReturnValue(new Promise(() => undefined)); + const view = renderInProvider(() => useIdentify()); + + let outcome: IdentifyResult | null | undefined; + act(() => { + void view.value.identify({ timeout: 3000 }).then((value) => { + outcome = value; + }); + }); + await act(() => vi.advanceTimersByTimeAsync(2999)); + expect(outcome).toBeUndefined(); + await act(() => vi.advanceTimersByTimeAsync(1)); + expect(outcome).toBeNull(); + expect(view.value).toMatchObject({ isLoading: false, error: { code: 'timeout' } }); + expect(vi.getTimerCount()).toBe(0); + }); + + it('gives the agent only what is left of the call timeout after the wait', async () => { + vi.useFakeTimers(); + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify } = createAgent(); + const view = renderInProvider(() => useIdentify({ userId: USER_HID })); + + act(() => { + void view.value.identify({ timeout: 3000 }); + }); + await act(() => vi.advanceTimersByTimeAsync(1200)); + await act(async () => { + pending.resolve(agent); + await vi.advanceTimersByTimeAsync(0); + }); + expect(identify).toHaveBeenCalledWith({ timeout: 1800, userId: USER_HID }); + expect(view.value).toMatchObject({ result: { userId: USER_HID }, isLoading: false }); + }); +}); + +describe('useIdentify({ runOnMount: true })', () => { + it('is loading from the first render and identifies once when the agent is ready', async () => { + const pending = deferred(); + loadMock.mockReturnValue(pending.promise); + const { agent, identify } = createAgent(); + const view = renderInProvider(() => useIdentify({ runOnMount: true, userId: USER_HID })); + + expect(view.value.isLoading).toBe(true); + expect(identify).not.toHaveBeenCalled(); + + pending.resolve(agent); + await flush(); + expect(identify).toHaveBeenCalledTimes(1); + // It waited for the load, so the agent gets what is left of the 10-second default timeout. + const [agentOptions] = identify.mock.calls[0]!; + expect(agentOptions).toStrictEqual({ userId: USER_HID, timeout: expect.any(Number) as number }); + expect(agentOptions?.timeout).toBeGreaterThan(9000); + expect(agentOptions?.timeout).toBeLessThanOrEqual(10000); + expect(view.value).toMatchObject({ result: { userId: USER_HID }, isLoading: false, error: null }); + }); + + it('does not identify again on re-renders, option changes or provider updates', async () => { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + let options: UseIdentifyOptions = { runOnMount: true, userId: USER_HID }; + const view = renderInProvider(() => useIdentify(options)); + await flush(); + expect(fake.identify).toHaveBeenCalledTimes(1); + + view.rerender(); + options = { runOnMount: true, userId: OTHER_USER_HID }; + view.rerender(); + view.rerender({ checkOnLoad: false }); + await flush(); + expect(fake.identify).toHaveBeenCalledTimes(1); + }); + + it('runs again for a new mount of the component', async () => { + const fake = createAgent(); + loadMock.mockResolvedValue(fake.agent); + const first = renderInProvider(() => useIdentify({ runOnMount: true })); + await flush(); + first.unmount(); + renderInProvider(() => useIdentify({ runOnMount: true })); + await flush(); + expect(fake.identify).toHaveBeenCalledTimes(2); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..ec0cb9d --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json index ad15890..f2b38e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,21 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2019", + "lib": ["ES2019", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", - "lib": ["ES2020", "DOM"], - "declaration": true, + "jsx": "react-jsx", + "types": ["node"], "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, "skipLibCheck": true, - "outDir": "dist" + "noEmit": true }, - "include": ["src"] + "include": ["src", "test", "*.ts"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..39b3b03 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { index: 'src/index.ts' }, + format: ['esm', 'cjs'], + dts: true, + clean: true, + target: 'es2019', + tsconfig: 'tsconfig.build.json', + // React and @shieldlabs-ai/js are peer dependencies and stay external. + external: ['react', '@shieldlabs-ai/js'], + // Marks every built file as a client module, so the Next.js App Router can import the package + // from server components. The directive must be the first statement of each output file. + banner: { js: '"use client";' }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..3f2c3c7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'happy-dom', + include: ['test/**/*.test.{ts,tsx}'], + setupFiles: ['test/setup.ts'], + mockReset: true, + restoreMocks: true, + unstubGlobals: true, + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + reporter: ['text', 'json-summary'], + thresholds: { + lines: 90, + statements: 90, + functions: 90, + branches: 90, + }, + }, + }, +});